From 41379460724522e7eb76babe43529a3fb699eaad Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Tue, 3 Nov 2015 10:16:25 +0100 Subject: [PATCH 001/306] removed NumberTickingAnimation from LiveAverageComponent --- .../LiveAverageComponent.java | 26 ++----------------- .../StartlineAdvantagesByWindComponent.java | 5 ++-- 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/LiveAverageComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/LiveAverageComponent.java index 9afcaf508d3..b0d3867c232 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/LiveAverageComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/LiveAverageComponent.java @@ -12,7 +12,6 @@ import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.dashboards.gwt.client.visualeffects.NumberTickingAnimation; import com.sap.sailing.dashboards.gwt.client.windchart.VerticalWindChartClickListener; import com.sap.sailing.dashboards.gwt.client.windchart.WindBotComponent; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -74,8 +73,6 @@ public class LiveAverageComponent extends Composite implements HasWidgets, Verti LiveAverageComponentStyle style; private StringMessages stringConstants; - private NumberTickingAnimation liveTickingAnimation; - private NumberTickingAnimation averageTickingAnimation; public LiveAverageComponent() { initWidget(uiBinder.createAndBindUi(this)); @@ -90,7 +87,6 @@ public class LiveAverageComponent extends Composite implements HasWidgets, Verti * */ public LiveAverageComponent(String header, String unit) { stringConstants = StringMessages.INSTANCE; - initTickingAnimations(); initWidget(uiBinder.createAndBindUi(this)); liveAveragePanel.getElement().getStyle().setProperty("backgroundColor", "white"); this.header.setInnerText(header); @@ -104,31 +100,13 @@ public class LiveAverageComponent extends Composite implements HasWidgets, Verti this.liveLabel.setInnerHTML(stringConstants.dashboardLiveWind()); this.averageLabel.setInnerHTML(stringConstants.dashboardAverageWind()+"
"+stringConstants.dashboardAverageWindMinutes(15)); } - - private void initTickingAnimations() { - liveTickingAnimation = new NumberTickingAnimation() { - - @Override - public void setValueInUI(String value) { - liveNumber.setInnerText(value); - } - }; - - averageTickingAnimation = new NumberTickingAnimation() { - - @Override - public void setValueInUI(String value) { - averageNumber.setInnerText(value); - } - }; - } public void setLiveValue(String liveValue) { - liveTickingAnimation.execute(liveValue); + liveNumber.setInnerText(liveValue); } public void setAverageValue(String averageValue) { - averageTickingAnimation.execute(averageValue); + averageNumber.setInnerText(averageValue); } @Override diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java index d29bf87d175..b5b93d2a6c1 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java @@ -7,6 +7,7 @@ import java.util.logging.Logger; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.resources.client.CssResource; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; @@ -77,8 +78,8 @@ public class StartlineAdvantagesByWindComponent extends Composite implements Has if (result != null) { logger.log(Level.INFO, "Updating UI with StartlineAdvantagesWithMaxAndAverageDTO"); startlineAdvantagesOnLineChart.setStartlineAdvantages(result.advantages); - advantageMaximumLiveAverage.setLiveValue("" + result.maximum); - advantageMaximumLiveAverage.setAverageValue("" + result.average); + advantageMaximumLiveAverage.setLiveValue(NumberFormat.getFormat("#0.0").format(result.maximum)); + advantageMaximumLiveAverage.setAverageValue(NumberFormat.getFormat("#0.0").format(result.average)); } else { logger.log(Level.INFO, "StartlineAdvantagesWithMaxAndAverageDTO is null"); } From 661d75c542cbd8ee5bb4fa69ce51a5ae43e50bb4 Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Wed, 11 Nov 2015 17:46:20 +0100 Subject: [PATCH 002/306] added DashboardURLParameters Enum and moved requesting of current live race id in own RibdashboadServiceImpl method --- .../gwt/client/RibDashboardPanel.java | 60 +++++++++++++----- .../gwt/client/RibDashboardService.java | 3 + .../gwt/client/RibDashboardServiceAsync.java | 2 + .../RibDashboardDataRetriever.java | 63 +------------------ .../dataretriever/WindBotDataRetriever.java | 7 +-- .../StartlineAnalysisComponent.java | 10 +-- .../gwt/server/RibDashboardServiceImpl.java | 18 ++++-- .../gwt/shared/DashboardURLParameters.java | 28 +++++++++ .../shared/dto/RibDashboardRaceInfoDTO.java | 2 - 9 files changed, 99 insertions(+), 94 deletions(-) create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardURLParameters.java diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java index 6073c35555e..aefb8c550eb 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java @@ -1,6 +1,7 @@ package com.sap.sailing.dashboards.gwt.client; import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -10,7 +11,7 @@ import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Document; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; -import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.Image; @@ -19,7 +20,6 @@ import com.google.gwt.user.client.ui.Widget; import com.googlecode.mgwt.ui.client.widget.carousel.Carousel; import com.sap.sailing.dashboards.gwt.client.dataretriever.NumberOfWindBotsChangeListener; import com.sap.sailing.dashboards.gwt.client.dataretriever.RibDashboardDataRetriever; -import com.sap.sailing.dashboards.gwt.client.dataretriever.RibDashboardDataRetrieverListener; import com.sap.sailing.dashboards.gwt.client.dataretriever.WindBotDataRetrieverProvider; import com.sap.sailing.dashboards.gwt.client.eventlogo.EventLogo; import com.sap.sailing.dashboards.gwt.client.notifications.WrongDeviceOrientationNotification; @@ -27,9 +27,13 @@ import com.sap.sailing.dashboards.gwt.client.startanalysis.StartlineAnalysisComp import com.sap.sailing.dashboards.gwt.client.startlineadvantage.StartLineAdvantageByGeometryComponent; import com.sap.sailing.dashboards.gwt.client.startlineadvantage.StartlineAdvantagesByWindComponent; import com.sap.sailing.dashboards.gwt.client.windchart.WindBotComponent; -import com.sap.sailing.dashboards.gwt.shared.dto.RibDashboardRaceInfoDTO; +import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; +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.sse.gwt.client.player.TimeListener; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.player.Timer.PlayModes; /** * This class contains all components of the Dashboard and the basic UI Elements of the Dashboard, like top and bottom @@ -38,8 +42,7 @@ import com.sap.sailing.gwt.ui.client.StringMessages; * @author Alexander Ries * */ -public class RibDashboardPanel extends Composite implements RibDashboardDataRetrieverListener, - NumberOfWindBotsChangeListener { +public class RibDashboardPanel extends Composite implements NumberOfWindBotsChangeListener { interface RootUiBinder extends UiBinder { } @@ -87,12 +90,12 @@ public class RibDashboardPanel extends Composite implements RibDashboardDataRetr private List windBotComponents; private StringMessages stringConstants; - private final String EVENT_ID_PARAMETER = "eventId"; + private RibDashboardServiceAsync ribDashboardService; private static final Logger logger = Logger.getLogger(RibDashboardPanel.class.getName()); public RibDashboardPanel(RibDashboardServiceAsync ribDashboardService, SailingServiceAsync sailingServiceAsync, RibDashboardDataRetriever ribDashboardDataRetriever) { + this.ribDashboardService = ribDashboardService; windBotComponents = new ArrayList(); - ribDashboardDataRetriever.addDataObserver(this); startanalysisComponent = new StartlineAnalysisComponent(ribDashboardService, sailingServiceAsync); startlineAdvantagesByWindComponent = new StartlineAdvantagesByWindComponent(ribDashboardService); startlineAdvantageByGeometryComponent = new StartLineAdvantageByGeometryComponent(ribDashboardDataRetriever); @@ -102,6 +105,7 @@ public class RibDashboardPanel extends Composite implements RibDashboardDataRetr windcharthint.getElement().setInnerText(stringConstants.dashboardWindChartHint()); windloadinghintleft.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
"+stringConstants.dashboardWindBotLoadingMessage()); windloadinghintright.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
"+stringConstants.dashboardWindBotLoadingMessage()); + startUpdatingRaceLabelIn20SecondInterval(); loadAndAddEventLogo(sailingServiceAsync); initAndAddWrongOrientationNotification(); } @@ -114,7 +118,7 @@ public class RibDashboardPanel extends Composite implements RibDashboardDataRetr } private void loadAndAddEventLogo(SailingServiceAsync sailingServiceAsync){ - String eventId = Window.Location.getParameter(EVENT_ID_PARAMETER); + String eventId = DashboardURLParameters.EVENT_ID.getValue(); if (eventId != null) { EventLogo eventLogo = EventLogo.getEventLogoFromEventId(sailingServiceAsync, eventId); if (eventLogo != null) { @@ -170,14 +174,40 @@ public class RibDashboardPanel extends Composite implements RibDashboardDataRetr } } - @Override - public void updateUIWithNewLiveRaceInfo(RibDashboardRaceInfoDTO liveRaceInfoDTO) { - if (liveRaceInfoDTO != null && liveRaceInfoDTO.idOfLastTrackedRace != null) { - logger.log(Level.INFO, "Updating UI with RibDashboardRaceInfoDTO race name"); - this.header.getElement().setInnerText(liveRaceInfoDTO.idOfLastTrackedRace.getRaceName()); - } - } + private void startUpdatingRaceLabelIn20SecondInterval() { + Timer timer = new Timer(PlayModes.Live); + timer.setRefreshInterval(20000); + timer.addTimeListener( new TimeListener() { + + @Override + public void timeChanged(Date newTime, Date oldTime) { + String leaderboardNameParameterValue = DashboardURLParameters.LEADERBOARD_NAME.getValue(); + if (leaderboardNameParameterValue != null) { + ribDashboardService.getIDFromRaceThatIsLive(leaderboardNameParameterValue, + new AsyncCallback() { + @Override + public void onSuccess(RegattaAndRaceIdentifier result) { + if (result != null) { + logger.log(Level.INFO, "Updating UI with RibDashboardRaceInfoDTO race name"); + setHeaderText(result.getRaceName()); + } + } + @Override + public void onFailure(Throwable caught) { + logger.log(Level.INFO, "Failed to received RegattaAndRaceIdentifier from race that is live, " + caught.getMessage()); + } + }); + } + } + }); + timer.play(); + } + + private void setHeaderText(String raceName) { + this.header.getElement().setInnerText(raceName); + } + @Override public void numberOfWindBotsReceivedChanged(List windBotIDs, WindBotDataRetrieverProvider windBotDataRetrieverProvider) { diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardService.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardService.java index b7dc461ca98..92bd68a8263 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardService.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardService.java @@ -11,6 +11,9 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.dto.CompetitorDTO; public interface RibDashboardService extends RemoteService { + + RegattaAndRaceIdentifier getIDFromRaceThatIsLive(String leaderboardName); + RibDashboardRaceInfoDTO getLiveRaceInfo(String leaderboardName) throws NoWindException; RegattaAndRaceIdentifier getIDFromRaceThatTakesWindFixesNow(String leaderboardName); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardServiceAsync.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardServiceAsync.java index 11dd9508bcf..d2def0e6f24 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardServiceAsync.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardServiceAsync.java @@ -11,6 +11,8 @@ import com.sap.sailing.domain.common.dto.CompetitorDTO; public interface RibDashboardServiceAsync { + void getIDFromRaceThatIsLive(String leaderboardName, AsyncCallback callback); + void getLiveRaceInfo(String leaderboardName, AsyncCallback callback); void getStartAnalysisListForCompetitorIDAndLeaderboardName(String competitorIdAsString, String leaderboardName, AsyncCallback> callback); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java index 7ef3fbd6c27..0c54183bd04 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java @@ -1,20 +1,16 @@ package com.sap.sailing.dashboards.gwt.client.dataretriever; import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; import com.sap.sailing.dashboards.gwt.client.actions.GetRibDashboardRaceInfoAction; +import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; import com.sap.sailing.dashboards.gwt.shared.dto.RibDashboardRaceInfoDTO; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.player.TimeListener; @@ -26,11 +22,9 @@ import com.sap.sse.gwt.client.player.TimeListener; * @author Alexander Ries * */ -public class RibDashboardDataRetriever implements TimeListener, RaceSelectionProvider { +public class RibDashboardDataRetriever implements TimeListener { - private String leaderboardName; private ArrayList dataRetrieverListener; - private ArrayList raceSelectionChangeListener; private AsyncActionsExecutor asyncActionsExecutor; @@ -38,25 +32,22 @@ public class RibDashboardDataRetriever implements TimeListener, RaceSelectionPro private final Object MUTEX; - private static final String PARAM_LEADERBOARD_NAME = "leaderboardName"; private static final Logger logger = Logger.getLogger(RibDashboardDataRetriever.class.getName()); public RibDashboardDataRetriever(RibDashboardServiceAsync ribDashboardService) { initNonFinalMemberVariablesWithNoArgumentConstructor(); MUTEX = new Object(); this.ribDashboardService = ribDashboardService; - this.leaderboardName = Window.Location.getParameter(PARAM_LEADERBOARD_NAME); asyncActionsExecutor = new AsyncActionsExecutor(); } private void initNonFinalMemberVariablesWithNoArgumentConstructor() { dataRetrieverListener = new ArrayList(); - raceSelectionChangeListener = new ArrayList(); } private void loadLiveRaceInfoFromRibDashboadService() { - GetRibDashboardRaceInfoAction getRibDashboardRaceInfoAction = new GetRibDashboardRaceInfoAction(ribDashboardService, leaderboardName); + GetRibDashboardRaceInfoAction getRibDashboardRaceInfoAction = new GetRibDashboardRaceInfoAction(ribDashboardService, DashboardURLParameters.LEADERBOARD_NAME.getValue()); logger.log(Level.INFO, "Executing GetRibDashboardRaceInfoAction"); asyncActionsExecutor.execute(getRibDashboardRaceInfoAction, new AsyncCallback() { @Override @@ -66,13 +57,6 @@ public class RibDashboardDataRetriever implements TimeListener, RaceSelectionPro switch (result.responseMessage) { case RACE_LIVE: logger.log(Level.INFO, "RibDashboardRaceInfoDTO.responseMessage is RACE_LIVE"); - if(result.idOfLastTrackedRace != null) { - logger.log(Level.INFO, "RibDashboardRaceInfoDTO idOfLastTrackedRace is "+result.idOfLastTrackedRace); - List singletonList = Collections.singletonList(result.idOfLastTrackedRace); - setSelection(singletonList); - } else { - logger.log(Level.INFO, "RibDashboardRaceInfoDTO.idOfLastTrackedRace is null"); - } notifyDataObservers(result); break; case NO_RACE_LIVE: @@ -123,45 +107,4 @@ public class RibDashboardDataRetriever implements TimeListener, RaceSelectionPro public void timeChanged(Date newTime, Date oldTime) { loadLiveRaceInfoFromRibDashboadService(); } - - @Override - public void addRaceSelectionChangeListener(RaceSelectionChangeListener listener) { - synchronized (MUTEX) { - if (listener != null) { - raceSelectionChangeListener.add(listener); - } - } - } - - @Override - public void removeRaceSelectionChangeListener(RaceSelectionChangeListener listener) { - synchronized (MUTEX) { - raceSelectionChangeListener.remove(listener); - } - } - - @Override - public void setSelection(List newSelection, - RaceSelectionChangeListener... listenersNotToNotify) { - synchronized (MUTEX) { - for (RaceSelectionChangeListener currentRaceSelectionChangeListener : raceSelectionChangeListener) { - currentRaceSelectionChangeListener.onRaceSelectionChange(newSelection); - } - } - } - - @Override - public List getAllRaces() { - return null; - } - - @Override - public void setAllRaces(List newAllRaces, - RaceSelectionChangeListener... listenersNotToNotify) { - } - - @Override - public List getSelectedRaces() { - return null; - } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java index 6f69bbca8c7..66008916713 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java @@ -8,10 +8,10 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; -import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; import com.sap.sailing.dashboards.gwt.client.actions.GetIDFromRaceThatTakesWindFixesNowAction; +import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; @@ -29,13 +29,11 @@ public class WindBotDataRetriever implements TimeListener, WindBotDataRetrieverP private List numberOfWindBotsChangeListeners; private List windBotDataRetrieverListeners; private List windBotIDsInLiveRace; - private String leaderboardName; private final Set windSourceTypeNames; private final int WIND_CHART_RESOLUTION_IN_MILLISECONDS = 5000; private final int ONE_HOUR_IN_MILLISECONDS = 1000*60*60; private final String LODA_WIND_CHART_DATA_CATEGORY = "loadWindChartData"; - private static final String PARAM_LEADERBOARD_NAME = "leaderboardName"; private boolean didInitialLoading; private static final Logger logger = Logger.getLogger(WindBotDataRetriever.class.getName()); @@ -49,7 +47,6 @@ public class WindBotDataRetriever implements TimeListener, WindBotDataRetrieverP windBotDataRetrieverListeners = new ArrayList(); windBotIDsInLiveRace = new ArrayList(); windSourceTypeNames = new HashSet<>(); - this.leaderboardName = Window.Location.getParameter(PARAM_LEADERBOARD_NAME); windSourceTypeNames.add(WindSourceType.EXPEDITION.name()); } @@ -146,7 +143,7 @@ public class WindBotDataRetriever implements TimeListener, WindBotDataRetrieverP public void timeChanged(Date newTime, Date oldTime) { final Date finalNewTime = newTime; final Date finaloldTime = oldTime; - GetIDFromRaceThatTakesWindFixesNowAction getIDFromRaceThatTakesWindFixesNowAction = new GetIDFromRaceThatTakesWindFixesNowAction(ribDashboardService, leaderboardName); + GetIDFromRaceThatTakesWindFixesNowAction getIDFromRaceThatTakesWindFixesNowAction = new GetIDFromRaceThatTakesWindFixesNowAction(ribDashboardService, DashboardURLParameters.LEADERBOARD_NAME.getValue()); logger.log(Level.INFO, "Executing GetIDFromRaceThatTakesWindFixesNowAction"); asyncActionsExecutor.execute(getIDFromRaceThatTakesWindFixesNowAction, new AsyncCallback() { @Override diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java index 9645a09aab4..438a9dc55a7 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java @@ -15,7 +15,6 @@ import com.google.gwt.resources.client.CssResource; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.Cookies; -import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FocusPanel; @@ -32,6 +31,7 @@ import com.sap.sailing.dashboards.gwt.client.notifications.BottomNotificationTyp import com.sap.sailing.dashboards.gwt.client.popups.competitorselection.CompetitorSelectionListener; import com.sap.sailing.dashboards.gwt.client.popups.competitorselection.CompetitorSelectionPopup; import com.sap.sailing.dashboards.gwt.client.popups.competitorselection.SettingsButtonWithSelectionIndicationLabel; +import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -112,9 +112,6 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets private CompetitorSelectionPopup competitorSelectionPopup; private SettingsButtonWithSelectionIndicationLabel settingsButtonWithSelectionIndicationLabel; - private String leaderboardName; - - private static final String PARAM_LEADERBOARD_NAME = "leaderboardName"; private static final String SELECTED_COMPETITOR_ID_COOKIE_KEY = "selectedCompetitorId"; private static final int SELECTED_COMPETITOR_ID_COOKIE_KEY_EXPIRE_TIME_IN_MILLIS = 60 * 1000 * 60 * 5; private static final int SCROLL_OFFSET_STARTANALYSIS_CARDS = 83; @@ -128,7 +125,6 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets this.sailingServiceAsync = sailingServiceAsync; pageChangeListener = new ArrayList(); starts = new ArrayList(); - this.leaderboardName = Window.Location.getParameter(PARAM_LEADERBOARD_NAME); initWidget(uiBinder.createAndBindUi(this)); @@ -177,7 +173,7 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets private void loadStartAnalysisDTOsForCompetitorID(String competitorIdAsString){ logger.log(Level.INFO, "Loading startanalysis for competitor id " + competitorIdAsString); ribDashboardServiceAsync.getStartAnalysisListForCompetitorIDAndLeaderboardName( - competitorIdAsString, leaderboardName, new AsyncCallback>() { + competitorIdAsString, DashboardURLParameters.LEADERBOARD_NAME.getValue(), new AsyncCallback>() { @Override public void onSuccess(List result) { logger.log(Level.INFO, "Received startanalysis list"); @@ -247,7 +243,7 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets private void loadCompetitorsAndShowCompetitorSelectionPopup() { logger.log(Level.INFO, "Requesting Competitors in Leaderboard"); - ribDashboardServiceAsync.getCompetitorsInLeaderboard(leaderboardName, new AsyncCallback>() { + ribDashboardServiceAsync.getCompetitorsInLeaderboard(DashboardURLParameters.LEADERBOARD_NAME.getValue(), new AsyncCallback>() { @Override public void onSuccess(List result) { logger.log(Level.INFO, "Received competitors for leaderboard name"); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/RibDashboardServiceImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/RibDashboardServiceImpl.java index 59452db071e..e79d721f70b 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/RibDashboardServiceImpl.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/RibDashboardServiceImpl.java @@ -89,6 +89,16 @@ public class RibDashboardServiceImpl extends RemoteServiceServlet implements Rib this.averageStartLineAdvantageByGeometry = new MovingAverage(400); } + @Override + public RegattaAndRaceIdentifier getIDFromRaceThatIsLive(String leaderboardName) { + RegattaAndRaceIdentifier result = null; + TrackedRace trackedRaceThatIsLive = getLiveRaceFromLeaderboardName(leaderboardName); + if(trackedRaceThatIsLive != null) { + result = trackedRaceThatIsLive.getRaceIdentifier(); + } + return result; + } + protected RacingEventService getRacingEventService() { return racingEventServiceTracker.getService(); } @@ -149,7 +159,6 @@ public class RibDashboardServiceImpl extends RemoteServiceServlet implements Rib private void fillLiveRaceInfoDTOWithRaceData(RibDashboardRaceInfoDTO lRInfo, TimePoint now) { if (runningRace != null) { - lRInfo.idOfLastTrackedRace = runningRace.getRaceIdentifier(); List competitors = runningRace.getCompetitorsFromBestToWorst(now); List competitorNames = new ArrayList(); for (Competitor competitor : competitors) { @@ -176,14 +185,13 @@ public class RibDashboardServiceImpl extends RemoteServiceServlet implements Rib private TrackedRace getLiveRaceFromLeaderboardName(String leaderboardName) { TrackedRace result = null; - Leaderboard lb = getRacingEventService().getLeaderboardByName(leaderboardName); - if (lb != null) { - for (RaceColumn column : lb.getRaceColumns()) { + Leaderboard leaderboard = getRacingEventService().getLeaderboardByName(leaderboardName); + if (leaderboard != null) { + for (RaceColumn column : leaderboard.getRaceColumns()) { for (Fleet fleet : column.getFleets()) { TrackedRace race = column.getTrackedRace(fleet); if (race != null && race.isLive(MillisecondsTimePoint.now())) { result = race; - notifyLiveTrackedRaceListenerAboutLiveTrackedRaceChange(race); } } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardURLParameters.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardURLParameters.java new file mode 100644 index 00000000000..04256a64ddf --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardURLParameters.java @@ -0,0 +1,28 @@ +package com.sap.sailing.dashboards.gwt.shared; + +import com.google.gwt.user.client.Window; + + +/** + * @author Alexander Ries (D062114) + * + */ +public enum DashboardURLParameters { + + EVENT_ID("eventId"), + LEADERBOARD_NAME("leaderboardName"); + + private String name; + + private DashboardURLParameters(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getValue() { + return Window.Location.getParameter(getName()); + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/RibDashboardRaceInfoDTO.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/RibDashboardRaceInfoDTO.java index bbc4c4039d7..9a714623d66 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/RibDashboardRaceInfoDTO.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/RibDashboardRaceInfoDTO.java @@ -9,7 +9,6 @@ import java.util.Map; import com.sap.sailing.dashboards.gwt.shared.ResponseMessage; import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisDTO; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; /** * @author Alexander Ries @@ -23,7 +22,6 @@ public class RibDashboardRaceInfoDTO implements Serializable{ public StartLineAdvantageDTO startLineAdvantageDTO; public List startAnalysisDTOList; public List competitorNamesFromLastTrackedRace; - public RegattaAndRaceIdentifier idOfLastTrackedRace; public ResponseMessage responseMessage; public RibDashboardRaceInfoDTO() {} From f046eef550e737d7b60ba48751f9327d67b41c02 Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Wed, 11 Nov 2015 18:32:53 +0100 Subject: [PATCH 003/306] renamed DashboardURLParameters field name to key --- .../gwt/shared/DashboardURLParameters.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardURLParameters.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardURLParameters.java index 04256a64ddf..80de5e507f7 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardURLParameters.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardURLParameters.java @@ -12,17 +12,17 @@ public enum DashboardURLParameters { EVENT_ID("eventId"), LEADERBOARD_NAME("leaderboardName"); - private String name; + private String key; - private DashboardURLParameters(String name) { - this.name = name; + private DashboardURLParameters(String key) { + this.key = key; } - public String getName() { - return name; + public String getKey() { + return key; } public String getValue() { - return Window.Location.getParameter(getName()); + return Window.Location.getParameter(getKey()); } } From 6d78aa877ff303d948050d9a66357f6bce72636b Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Thu, 12 Nov 2015 10:13:06 +0100 Subject: [PATCH 004/306] renamed GetStartlineAdvantagesAction --- ...gesAction.java => GetStartlineAdvantagesByWindAction.java} | 4 ++-- .../StartlineAdvantagesByWindComponent.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/{GetStartlineAdvantagesAction.java => GetStartlineAdvantagesByWindAction.java} (77%) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantagesAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantagesByWindAction.java similarity index 77% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantagesAction.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantagesByWindAction.java index 1cf2be86c81..d57753ab0ec 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantagesAction.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantagesByWindAction.java @@ -10,12 +10,12 @@ import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; import com.sap.sailing.dashboards.gwt.shared.dto.StartlineAdvantagesWithMaxAndAverageDTO; import com.sap.sse.gwt.client.async.AsyncAction; -public class GetStartlineAdvantagesAction implements AsyncAction { +public class GetStartlineAdvantagesByWindAction implements AsyncAction { private final RibDashboardServiceAsync ribDashboardService; private final String leaderboardName; - public GetStartlineAdvantagesAction(RibDashboardServiceAsync ribDashboardService, String leaderboardName) { + public GetStartlineAdvantagesByWindAction(RibDashboardServiceAsync ribDashboardService, String leaderboardName) { this.ribDashboardService = ribDashboardService; this.leaderboardName = leaderboardName; } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java index b5b93d2a6c1..25d7be9e90f 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java @@ -16,7 +16,7 @@ import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; -import com.sap.sailing.dashboards.gwt.client.actions.GetStartlineAdvantagesAction; +import com.sap.sailing.dashboards.gwt.client.actions.GetStartlineAdvantagesByWindAction; import com.sap.sailing.dashboards.gwt.client.dataretriever.WindBotDataRetriever; import com.sap.sailing.dashboards.gwt.shared.dto.StartlineAdvantagesWithMaxAndAverageDTO; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -67,7 +67,7 @@ public class StartlineAdvantagesByWindComponent extends Composite implements Has } private void loadData() { - GetStartlineAdvantagesAction getRibDashboardRaceInfoAction = new GetStartlineAdvantagesAction(ribDashboardService, ""); + GetStartlineAdvantagesByWindAction getRibDashboardRaceInfoAction = new GetStartlineAdvantagesByWindAction(ribDashboardService, ""); logger.log(Level.INFO, "Executing GetStartlineAdvantagesAction"); asyncActionsExecutor.execute(getRibDashboardRaceInfoAction, new AsyncCallback() { From aa1d932d23fac3d7d61c108e9d9bfd9c3ba12b4c Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Thu, 12 Nov 2015 13:21:21 +0100 Subject: [PATCH 005/306] bug1775: refresh button on LeaderboardGroupC fixed --- .../LeaderboardGroupConfigPanel.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java index ddf2a0ad41e..a0bc37b92ea 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java @@ -7,6 +7,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import com.google.gwt.cell.client.FieldUpdater; import com.google.gwt.core.client.GWT; @@ -480,8 +481,8 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements refreshButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { - leaderboardGroupsRefresher.fillLeaderboardGroups(); leaderboardsRefresher.fillLeaderboards(); + leaderboardGroupsRefresher.fillLeaderboardGroups(); } }); leaderboardGroupsControlsPanel.add(refreshButton); @@ -591,6 +592,19 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements Util.addAll(groups, availableLeaderboardGroups); } groupsFilterablePanel.updateAll(availableLeaderboardGroups); + + UUID id = getSelectedGroup().getId(); + if (id != null) { + groupsSelectionModel.clear(); + for (LeaderboardGroupDTO leaderboardGroup : availableLeaderboardGroups) { + if (id.equals(leaderboardGroup.getId())) { + groupsSelectionModel.setSelected(leaderboardGroup, true); + } else { + groupsSelectionModel.setSelected(leaderboardGroup, false); + } + } + groupSelectionChanged(); + } } @Override @@ -812,12 +826,16 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements groupDetailsSelectionModel.clear(); groupDetailsProvider.getList().clear(); groupDetailsProvider.getList().addAll(selectedGroup.leaderboards); + groupDetailsProvider.refresh(); // Tell leaderboards filter panel to use all available leaderboards except the leaderboards of the group from the list leaderboardsSelectionModel.clear(); ArrayList allLeaderboardsExceptThoseOfSelectedLeaderboardGroup = new ArrayList<>(availableLeaderboards); allLeaderboardsExceptThoseOfSelectedLeaderboardGroup.removeAll(selectedGroup.leaderboards); leaderboardsFilterablePanel.updateAll(allLeaderboardsExceptThoseOfSelectedLeaderboardGroup); + leaderboardsProvider.getList().clear(); + leaderboardsProvider.getList().addAll(allLeaderboardsExceptThoseOfSelectedLeaderboardGroup); + leaderboardsProvider.refresh(); } } From 07537e650cdd47294ec58f03cefeef3fb5c9a297 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 16 Nov 2015 15:19:07 +0100 Subject: [PATCH 006/306] First steps to make SelectionModels refreshable --- .../ui/adminconsole/EventListComposite.java | 26 ++++---- .../controls/SelectionCheckboxColumn.java | 10 +-- java/com.sap.sse.gwt/META-INF/MANIFEST.MF | 1 + .../client/celltable/HasEqualIdentity.java | 8 +++ .../RefreshableMultiSelectionModel.java | 63 +++++++++++++++++++ .../celltable/RefreshableSelectionModel.java | 12 ++++ .../RefreshableSingleSelectionModel.java | 63 +++++++++++++++++++ 7 files changed, 163 insertions(+), 20 deletions(-) create mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/HasEqualIdentity.java create mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java create mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java create mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 6a9820f3bf8..0ce31f366d7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -7,7 +7,6 @@ import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.UUID; import com.google.gwt.cell.client.AbstractCell; @@ -32,7 +31,6 @@ import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.MultiSelectionModel; import com.google.gwt.view.client.SelectionChangeEvent; import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; import com.sap.sailing.gwt.ui.client.EventSelectionProvider; @@ -50,6 +48,8 @@ import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.HasEqualIdentity; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -65,7 +65,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le private final StringMessages stringMessages; private CellTable eventTable; - private MultiSelectionModel eventSelectionModel; + private RefreshableMultiSelectionModel eventSelectionModel; private ListDataProvider eventListDataProvider; private List allEvents; private LabeledAbstractFilterablePanel filterTextbox; @@ -146,7 +146,13 @@ public class EventListComposite extends Composite implements EventsRefresher, Le eventTable.setVisible(false); @SuppressWarnings("unchecked") - MultiSelectionModel multiSelectionModel = (MultiSelectionModel) eventTable.getSelectionModel(); + RefreshableMultiSelectionModel multiSelectionModel = (RefreshableMultiSelectionModel) eventTable.getSelectionModel(); + multiSelectionModel.setHasEqualIdentity(new HasEqualIdentity() { + @Override + public boolean compare(EventDTO o1, EventDTO o2) { + return o1.id.equals(o2.id) ? true : false; + } + }); eventSelectionModel = multiSelectionModel; eventSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { @@ -601,20 +607,10 @@ public class EventListComposite extends Composite implements EventsRefresher, Le eventTable.setVisible(true); noEventsLabel.setVisible(false); } - - Set selectedEventUUIDs = new HashSet<>(); - for (EventDTO selectedEvent : eventSelectionModel.getSelectedSet()) { - selectedEventUUIDs.add(selectedEvent.id); - } - eventSelectionModel.clear(); + eventSelectionModel.refreshSelectionModel(events); allEvents.clear(); allEvents.addAll(events); filterTextbox.updateAll(allEvents); - for (EventDTO e : allEvents) { - if (selectedEventUUIDs.contains(e.id)) { - eventSelectionModel.setSelected(e, true); - } - } } }); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java index d328719ca5e..d2c27f7a405 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java @@ -15,11 +15,11 @@ import com.google.gwt.view.client.DefaultSelectionEventManager; import com.google.gwt.view.client.DefaultSelectionEventManager.EventTranslator; import com.google.gwt.view.client.DefaultSelectionEventManager.SelectAction; import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.MultiSelectionModel; import com.sap.sailing.domain.common.InvertibleComparator; import com.sap.sailing.domain.common.SortingOrder; import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; import com.sap.sailing.domain.common.impl.InvertibleComparatorAdapter; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; /** * A column to be used in a {@link CellTable} that controls and reflects a table's selection model using stylable @@ -47,7 +47,7 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW private final BetterCheckboxCell cell; private final String checkboxColumnCellCSSClass; private final EventTranslator selectionEventTranslator; - private final MultiSelectionModel selectionModel; + private final RefreshableMultiSelectionModel selectionModel; /** * @param selectedCheckboxCSSClass @@ -86,7 +86,7 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW return DefaultSelectionEventManager.createCustomManager(getSelectionEventTranslator()); } - public MultiSelectionModel getSelectionModel() { + public RefreshableMultiSelectionModel getSelectionModel() { return selectionModel; } @@ -96,8 +96,8 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW * Otherwise, clients or subclasses are responsible to issue the necessary calls to {@link #redrawRow(LeaderboardRowDTO, List)} * after selection changes. */ - private MultiSelectionModel createSelectionModel() { - return new MultiSelectionModel() { + private RefreshableMultiSelectionModel createSelectionModel() { + return new RefreshableMultiSelectionModel() { @Override public void clear() { super.clear(); diff --git a/java/com.sap.sse.gwt/META-INF/MANIFEST.MF b/java/com.sap.sse.gwt/META-INF/MANIFEST.MF index 1b53b52f991..0d827bce8f4 100644 --- a/java/com.sap.sse.gwt/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.gwt/META-INF/MANIFEST.MF @@ -20,6 +20,7 @@ Export-Package: com.google.gwt.user.client.rpc.core.java.util, com.sap.sse.common.impl, com.sap.sse.gwt.client, com.sap.sse.gwt.client.async, + com.sap.sse.gwt.client.celltable, com.sap.sse.gwt.client.controls, com.sap.sse.gwt.client.controls.busyindicator, com.sap.sse.gwt.client.controls.carousel, diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/HasEqualIdentity.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/HasEqualIdentity.java new file mode 100644 index 00000000000..f144c9ebcb0 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/HasEqualIdentity.java @@ -0,0 +1,8 @@ +package com.sap.sse.gwt.client.celltable; + +public interface HasEqualIdentity { + /** + * Compares the Objects and return true , if the objects have the same Identity. + */ + public boolean compare(T o1, T o2); +} \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java new file mode 100644 index 00000000000..ea3acd394f2 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -0,0 +1,63 @@ +package com.sap.sse.gwt.client.celltable; + +import java.util.Set; + +import com.google.gwt.view.client.MultiSelectionModel; +import com.google.gwt.view.client.ProvidesKey; + +public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel { + private HasEqualIdentity comp; + + public RefreshableMultiSelectionModel() { + super(); + comp = null; + } + + public RefreshableMultiSelectionModel(HasEqualIdentity comp) { + super(); + this.comp = comp; + } + + public RefreshableMultiSelectionModel(ProvidesKey keyProvider, HasEqualIdentity comp) { + super(keyProvider); + this.comp = comp; + } + + @Override + public HasEqualIdentity getHasEqualIdentity() { + return comp; + } + + @Override + public void setHasEqualIdentity(HasEqualIdentity comp) { + this.comp = comp; + } + + @Override + public void refreshSelectionModel(Iterable newObjects) { + Set selectedSet = getSelectedSet(); + HasEqualIdentity comp = getHasEqualIdentity(); + clear(); + if (comp != null) { + for(T it : newObjects) { + for(T selected : selectedSet) { + if(comp.compare(selected, it)) { + setSelected(it, true); + } else { + setSelected(it, false); + } + } + } + } else { + for(T it : newObjects) { + for(T selected : selectedSet) { + if(selected.equals(it)) { + setSelected(it, true); + } else { + setSelected(it, false); + } + } + } + } + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java new file mode 100644 index 00000000000..d927abaddf0 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java @@ -0,0 +1,12 @@ +package com.sap.sse.gwt.client.celltable; + +import java.util.Set; + +public interface RefreshableSelectionModel { + public HasEqualIdentity getHasEqualIdentity(); + public void setHasEqualIdentity(HasEqualIdentity comp); + public Set getSelectedSet(); + public void clear(); + public void setSelected(T item, boolean selected); + public void refreshSelectionModel(Iterable newObjects); +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java new file mode 100644 index 00000000000..a6be572e965 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -0,0 +1,63 @@ +package com.sap.sse.gwt.client.celltable; + +import java.util.Set; + +import com.google.gwt.view.client.ProvidesKey; +import com.google.gwt.view.client.SingleSelectionModel; + +public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel { + private HasEqualIdentity comp; + + public RefreshableSingleSelectionModel() { + super(); + comp = null; + } + + public RefreshableSingleSelectionModel(HasEqualIdentity comp) { + super(); + this.comp=comp; + } + + public RefreshableSingleSelectionModel(ProvidesKey keyProvider, HasEqualIdentity comp) { + super(keyProvider); + this.comp =comp; + } + + @Override + public HasEqualIdentity getHasEqualIdentity() { + return comp; + } + + @Override + public void setHasEqualIdentity(HasEqualIdentity comp) { + this.comp = comp; + } + + @Override + public void refreshSelectionModel(Iterable newObjects) { + Set selectedSet = getSelectedSet(); + HasEqualIdentity comp = getHasEqualIdentity(); + clear(); + if (comp != null) { + for(T it : newObjects) { + for(T selected : selectedSet) { + if(comp.compare(selected, it)) { + setSelected(it, true); + } else { + setSelected(it, false); + } + } + } + } else { + for(T it : newObjects) { + for(T selected : selectedSet) { + if(selected.equals(it)) { + setSelected(it, true); + } else { + setSelected(it, false); + } + } + } + } + } +} From a4db261f5c6a36eb1f9a62964d5f37cd76f39cf9 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 16 Nov 2015 15:59:08 +0100 Subject: [PATCH 007/306] Added JavaDoc --- .../celltable/RefreshableSelectionModel.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java index d927abaddf0..b54fbe8210a 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java @@ -3,10 +3,33 @@ package com.sap.sse.gwt.client.celltable; import java.util.Set; public interface RefreshableSelectionModel { + /** + * @return Returns an instance of HasEqualIdentity<T>, to compare objects from the type + * <T>. + */ public HasEqualIdentity getHasEqualIdentity(); + + /** + * Sets the HasEqualIdentity<T> field, to compare objects from the type <T>. + * + * @param comp + * When you set null, the T.eqauls() will be used. + */ public void setHasEqualIdentity(HasEqualIdentity comp); + public Set getSelectedSet(); + public void clear(); + public void setSelected(T item, boolean selected); + + /** + * Refreshes the RefreshableSelectionModel<T> with the newObjects. If the selected + * objects are according to HasEqualIdentity<T>.compare() are the same, the selection will be + * reselected. If the object have no HasEqualIdentity<T>, this method will use the + * <T>.eqauls() method to compare. + * + * @param newObjects + */ public void refreshSelectionModel(Iterable newObjects); } From a3d7af5bf02ee91f79244362d4d3e5becb1d22b9 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 16 Nov 2015 18:14:48 +0100 Subject: [PATCH 008/306] coding conventions --- .../com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 11c7854898e..1f8a91c7bab 100755 --- 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 @@ -344,7 +344,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS private Element elementToBlur; private boolean showSelectionCheckbox; - private List leaderboardUpdateListener; + private final List leaderboardUpdateListener; private boolean initialCompetitorFilterHasBeenApplied = false; private final boolean showCompetitorFilterStatus; @@ -3197,7 +3197,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS if (timer != null) { timer.removeTimeListener(this); } - if(leaderboardUpdateListener != null) { + if (leaderboardUpdateListener != null) { leaderboardUpdateListener.clear(); } } From f3043d5937626bfdfd1d0ee51025f8210aeffb03 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 16 Nov 2015 18:15:13 +0100 Subject: [PATCH 009/306] allow RaceBoardPanel to operate without competitor chart and wind chart and mark passing editing panel is screen is small; bug 3345 --- .../sap/sailing/gwt/ui/client/TimePanel.java | 6 +- .../gwt/ui/raceboard/RaceBoardPanel.java | 61 +++++++++---------- .../raceboard/SideBySideComponentViewer.java | 37 ++++++----- .../ui/raceboard/TouchSplitLayoutPanel.java | 4 +- 4 files changed, 52 insertions(+), 56 deletions(-) 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 b87c3903017..4702b1588de 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 @@ -105,7 +105,11 @@ public class TimePanel extends SimplePanel implemen timePanelSliderFlowWrapper = new FlowPanel(); timePanelSlider.setStyleName("timePanelSlider"); timePanelSlider.getElement().getStyle().setPaddingLeft(66, Unit.PX); - timePanelSlider.getElement().getStyle().setPaddingRight(66, Unit.PX); + // TODO pass this in as a parameter; bug 3345 + boolean isScreenLargeEnoughToOfferChartSupport = true; + if (isScreenLargeEnoughToOfferChartSupport) { + timePanelSlider.getElement().getStyle().setPaddingRight(66, Unit.PX); + } timePanelSliderFlowWrapper.add(timePanelSlider); playSpeedImg = resources.timesliderPlaySpeedIcon(); 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 de8f6f7af61..06c65bf3a1b 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 @@ -207,23 +207,27 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi raceTimesInfoProvider.addRaceTimesInfoProviderListener(raceMap); raceMap.onRaceSelectionChange(Collections.singletonList(selectedRaceIdentifier)); List> components = new ArrayList>(); - competitorChart = new MultiCompetitorRaceChart(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, - timer, timeRangeWithZoomModel, stringMessages, errorReporter, true, true, leaderboardGroupName, leaderboardName); - competitorChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); - competitorChart.getEntryWidget().setTitle(stringMessages.competitorCharts()); - competitorChart.setVisible(false); - components.add(competitorChart); - windChart = new WindChart(sailingService, raceSelectionProvider, timer, timeRangeWithZoomModel, new WindChartSettings(), - stringMessages, asyncActionsExecutor, errorReporter, /* compactChart */ true); - windChart.setVisible(false); - windChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); - windChart.getEntryWidget().setTitle(stringMessages.windChart()); - components.add(windChart); - editMarkPassingPanel = new EditMarkPassingsPanel(sailingService, selectedRaceIdentifier, - stringMessages, competitorSelectionProvider, errorReporter, timer); - editMarkPassingPanel.setLeaderboardNameAndColumn(leaderboardPanel.getLeaderboard()); - editMarkPassingPanel.getEntryWidget().setTitle(stringMessages.editMarkPassings()); - components.add(editMarkPassingPanel); + // TODO pass this in as a parameter; bug 3345 + boolean isScreenLargeEnoughToOfferChartSupport = true; + if (isScreenLargeEnoughToOfferChartSupport) { + competitorChart = new MultiCompetitorRaceChart(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, + timer, timeRangeWithZoomModel, stringMessages, errorReporter, true, true, leaderboardGroupName, leaderboardName); + competitorChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); + competitorChart.getEntryWidget().setTitle(stringMessages.competitorCharts()); + competitorChart.setVisible(false); + components.add(competitorChart); + windChart = new WindChart(sailingService, raceSelectionProvider, timer, timeRangeWithZoomModel, new WindChartSettings(), + stringMessages, asyncActionsExecutor, errorReporter, /* compactChart */ true); + windChart.setVisible(false); + windChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); + windChart.getEntryWidget().setTitle(stringMessages.windChart()); + components.add(windChart); + editMarkPassingPanel = new EditMarkPassingsPanel(sailingService, selectedRaceIdentifier, + stringMessages, competitorSelectionProvider, errorReporter, timer); + editMarkPassingPanel.setLeaderboardNameAndColumn(leaderboardPanel.getLeaderboard()); + editMarkPassingPanel.getEntryWidget().setTitle(stringMessages.editMarkPassings()); + components.add(editMarkPassingPanel); + } boolean autoSelectMedia = getConfiguration().isAutoSelectMedia(); MediaPlayerManagerComponent mediaPlayerManagerComponent = new MediaPlayerManagerComponent( selectedRaceIdentifier, raceTimesInfoProvider, timer, mediaService, userService, stringMessages, @@ -238,8 +242,10 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi showLeaderboard = false; } setLeaderboardVisible(showLeaderboard); - setWindChartVisible(getConfiguration().isShowWindChart()); - setCompetitorChartVisible(getConfiguration().isShowCompetitorsChart()); + if (isScreenLargeEnoughToOfferChartSupport) { + setWindChartVisible(getConfiguration().isShowWindChart()); + setCompetitorChartVisible(getConfiguration().isShowCompetitorsChart()); + } // make sure to load leaderboard data for filtering to work if (!showLeaderboard) { leaderboardPanel.setVisible(true); @@ -345,7 +351,9 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi @Override public void updatedLeaderboard(LeaderboardDTO leaderboard) { leaderboardAndMapViewer.setLeftComponentWidth(leaderboardPanel.getContentPanel().getOffsetWidth()); - editMarkPassingPanel.setLeaderboardNameAndColumn(leaderboard); + if (editMarkPassingPanel != null) { + editMarkPassingPanel.setLeaderboardNameAndColumn(leaderboard); + } } @Override @@ -394,19 +402,6 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi } } regattaNameAnchor.setStyleName("RegattaName-Anchor"); - - // TODO: Strange behavior... check -// Window.addResizeHandler(new ResizeHandler() { -// @Override -// public void onResize(ResizeEvent event) { -// int headerPanelWidth = raceMap.getRightHeaderPanel().getOffsetWidth() - 150; // 150px is the width of the sapLogo -// int raceNameAndFleetLabelWidth = raceInformationHeader.getOffsetWidth(); -// int regattaAnchorWidth = regattaNameAnchor.getOffsetWidth(); -// boolean overlap = raceNameAndFleetLabelWidth + regattaAnchorWidth > headerPanelWidth; -// raceInformationHeader.setVisible(!overlap); -// } -// }); - Label raceTimeLabel = computeRaceInformation(raceColumn, fleet); raceTimeLabel.setStyleName("RaceTime-Label"); regattaAndRaceTimeInformationHeader.clear(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java index d6ba3b34ed7..21091699f71 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java @@ -24,6 +24,7 @@ import com.sap.sailing.gwt.ui.client.media.MediaPlayerManagerComponent; import com.sap.sailing.gwt.ui.client.media.MediaSingleSelectionControl; import com.sap.sailing.gwt.ui.client.shared.charts.EditMarkPassingsPanel; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; +import com.sap.sailing.gwt.ui.raceboard.TouchSplitLayoutPanel.Splitter; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.settings.AbstractSettings; import com.sap.sse.gwt.client.shared.components.Component; @@ -84,12 +85,9 @@ public class SideBySideComponentViewer implements ComponentViewer, UserStatusEve this.mediaSelectionButton = createMediaSelectionButton(mediaPlayerManagerComponent); this.mediaManagementButton = createMediaManagementButton(mediaPlayerManagerComponent); this.markPassingsPanel = markPassingsPanel; - userService.addUserStatusEventHandler(this); - mediaPlayerManagerComponent.setPlayerChangeListener(new PlayerChangeListener() { - - public void notifyStateChange(){ + public void notifyStateChange() { String caption; String tooltip; switch (mediaPlayerManagerComponent.getAssignedMediaTracks().size()) { @@ -124,7 +122,6 @@ public class SideBySideComponentViewer implements ComponentViewer, UserStatusEve mediaSelectionButton.setTitle(tooltip); mediaManagementButton.setVisible(mediaPlayerManagerComponent.allowsEditing()); } - }); this.leftScrollPanel = new ScrollPanel(); this.leftScrollPanel.add(leftComponentP.getEntryWidget()); @@ -133,8 +130,7 @@ public class SideBySideComponentViewer implements ComponentViewer, UserStatusEve this.mainPanel.setSize("100%", "100%"); this.mainPanel.getElement().getStyle().setMarginTop(-12, Unit.PX); this.mainPanel.setStyleName("SideBySideComponentViewer-MainPanel"); - this.splitLayoutPanel = new TouchSplitLayoutPanel(/* horizontal splitter width */3, /* vertical splitter height */ - 25); + this.splitLayoutPanel = new TouchSplitLayoutPanel(/* horizontal splitter width */3, /* vertical splitter height */ 25); this.mainPanel.add(splitLayoutPanel); // initialize components - they need to be added before other widgets to get the right width @@ -158,7 +154,6 @@ public class SideBySideComponentViewer implements ComponentViewer, UserStatusEve "managemedia")); } onUserStatusChange(userService.getCurrentUser()); - // ensure that toggle buttons are positioned right splitLayoutPanel.lastComponentHasBeenAdded(this, panelForMapAndHorizontalToggleButtons, additionalVerticalButtons); @@ -208,14 +203,12 @@ public class SideBySideComponentViewer implements ComponentViewer, UserStatusEve final MediaManagementControl multiSelectionControl = new MediaManagementControl(mediaPlayerManager, result, stringMessages); result.addClickHandler(new ClickHandler() { - @Override public void onClick(ClickEvent event) { multiSelectionControl.show(); } }); - - // hide button initially as we defer showing the button to the asynchroneous + // hide button initially as we defer showing the button to the asynchronous // task that gets launched by the media service to get video tracks result.setVisible(false); return result; @@ -291,15 +284,19 @@ public class SideBySideComponentViewer implements ComponentViewer, UserStatusEve @Override public void onUserStatusChange(UserDTO user) { - Button toggleButton = splitLayoutPanel.getAssociatedSplitter(markPassingsPanel).getToggleButton(); - if (user != null - && (user.hasRole(DefaultRoles.ADMIN.getRolename()) || user.hasRole(Roles.eventmanager.getRolename()))) { - toggleButton.setVisible(true); - forceLayout(); - } else { - markPassingsPanel.setVisible(false); - toggleButton.setVisible(false); - forceLayout(); + final Splitter associatedSplitter = splitLayoutPanel.getAssociatedSplitter(markPassingsPanel); + if (associatedSplitter != null) { // if the panel is not present, the splitter may not be found + final Button toggleButton = associatedSplitter.getToggleButton(); + if (user != null + && (user.hasRole(DefaultRoles.ADMIN.getRolename()) || + user.hasRole(Roles.eventmanager.getRolename()))) { + toggleButton.setVisible(true); + forceLayout(); + } else { + markPassingsPanel.setVisible(false); + toggleButton.setVisible(false); + forceLayout(); + } } } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java index 3f6b991c931..dfcff9fd58f 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java @@ -613,7 +613,7 @@ public class TouchSplitLayoutPanel extends DockLayoutPanel { if (lastVerticalSplitter != null) { Panel panel = createToggleButtonPanel(allVerticalSplitters, "gwt-SplitLayoutPanel-NorthSouthToggleButton-Panel", "gwt-SplitLayoutPanel-NorthSouthToggleButton", - cm, additionalVerticalButtonsAndStyles); + cm, additionalVerticalButtonsAndStyles); // TODO additional buttons will only be displayed if at least one vertical splitter is... on purpose? lastVerticalSplitter.addToogleButtons(panel); lastVerticalSplitter.setVisible(true); lastVerticalSplitter.setDraggerVisible(false); @@ -621,7 +621,7 @@ public class TouchSplitLayoutPanel extends DockLayoutPanel { ensureVerticalToggleButtonPosition(); if (lastHorizontalSplitter != null) { Panel horizontalButtonsPanel = createToggleButtonPanel(allHorizontalSplitters, - "gwt-SplitLayoutPanel-EastToggleButton-Panel", "gwt-SplitLayoutPanel-EastToggleButton", cm, null); + "gwt-SplitLayoutPanel-EastToggleButton-Panel", "gwt-SplitLayoutPanel-EastToggleButton", cm, /* additional buttons and styles */ null); panelForHorizontalButtons.add(horizontalButtonsPanel); lastHorizontalSplitter.setVisible(false); lastHorizontalSplitter.setDraggerVisible(false); From f4bc9afb7c3f71afd057f979d5d514fe288e16cc Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Tue, 17 Nov 2015 13:41:10 +0100 Subject: [PATCH 010/306] implemented refreshableSelectionModel for regattas --- .../gwt/ui/client/RegattaSelectionModel.java | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java index a416c372cff..f7e3aed53c3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java @@ -9,8 +9,10 @@ import java.util.List; import java.util.Set; import com.sap.sailing.domain.common.RegattaIdentifier; +import com.sap.sse.gwt.client.celltable.HasEqualIdentity; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; -public class RegattaSelectionModel implements RegattaSelectionProvider { +public class RegattaSelectionModel implements RegattaSelectionProvider, RefreshableSelectionModel { private final List selection; private final List allRegattas; @@ -91,4 +93,69 @@ public class RegattaSelectionModel implements RegattaSelectionProvider { public boolean hasMultiSelection() { return hasMultiSelection; } + + /** + * Returns an instance of HasEqualIdentity<RegattaIdentifier>. The compare + * method works the same way as o1.eqauls(o2). + */ + @Override + public HasEqualIdentity getHasEqualIdentity() { + return new HasEqualIdentity() { + @Override + public boolean compare(RegattaIdentifier o1, RegattaIdentifier o2) { + return o1.equals(o2); + } + }; + } + + /** + * This method will have no impact on the object, because the only logical way to + * compare RegattaIdentifier objects is to use the equals method. + */ + @Override + public void setHasEqualIdentity(HasEqualIdentity comp) { + //TODO + } + + @Override + public Set getSelectedSet() { + Iterable selected = getSelectedRegattas(); + Set result = new HashSet<>(); + for (RegattaIdentifier it : selected) { + result.add(it); + } + return result; + } + + @Override + public void clear() { + setAllRegattas(new ArrayList()); + } + + @Override + public void setSelected(RegattaIdentifier item, boolean selected) { + if (selected) { + List all = getAllRegattas(); + List selection = getSelectedRegattas(); + all.add(item); + selection.add(item); + setAllRegattas(all); + setSelection(selection); + } else { + List all = getAllRegattas(); + all.add(item); + setAllRegattas(all); + } + } + + @Override + public void refreshSelectionModel(Iterable newObjects) { + if(newObjects != null) { + List all = new ArrayList<>(); + for(RegattaIdentifier it : newObjects) { + all.add(it); + } + setAllRegattas(all); + } + } } From 30f1fa3e86dc829c0be9a49fb30a1396da8437ae Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 17 Nov 2015 23:07:52 +0100 Subject: [PATCH 011/306] pass through the decision whether charts would be shown, based on client height --- .../place/player/AutoPlayController.java | 6 ++--- .../sailing/gwt/ui/client/RaceTimePanel.java | 4 +-- .../sap/sailing/gwt/ui/client/TimePanel.java | 10 +++++--- .../EmbeddedMapAndWindChartEntryPoint.java | 3 ++- .../gwt/ui/raceboard/RaceBoardEntryPoint.java | 9 ++++--- .../gwt/ui/raceboard/RaceBoardPanel.java | 25 ++++++++++++------- .../gwt/ui/simulator/SimulatorMainPanel.java | 2 +- 7 files changed, 37 insertions(+), 22 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java index 5cfe4fe6697..841a2525cd9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java @@ -170,10 +170,10 @@ public class AutoPlayController implements RaceTimesInfoProviderListener { RaceSelectionModel raceSelectionModel = new RaceSelectionModel(); List singletonList = Collections.singletonList(raceToShow); raceSelectionModel.setSelection(singletonList); - RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, userService, asyncActionsExecutor, - competitorsAndTheirBoats, raceboardTimer, raceSelectionModel, leaderboardName, null, /* event */null, raceboardViewConfig, - errorReporter, StringMessages.INSTANCE, userAgent, raceTimesInfoProvider, /* showMapControls */false); + competitorsAndTheirBoats, raceboardTimer, raceSelectionModel, leaderboardName, null, /* event */ null, raceboardViewConfig, + errorReporter, StringMessages.INSTANCE, userAgent, raceTimesInfoProvider, /* showMapControls */ false, + /* isScreenLargeEnoughToOfferChartSupport */ true); return raceBoardPanel; } 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 ef6d167bb08..45e0b3b8de5 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 @@ -30,8 +30,8 @@ public class RaceTimePanel extends TimePanel implements R private boolean redrawAllMarkersPendingForMinMaxBeingInitialized; public RaceTimePanel(Timer timer, TimeRangeWithZoomProvider timeRangeProvider, StringMessages stringMessages, - RaceTimesInfoProvider raceTimesInfoProvider, boolean canReplayWhileLiveIsPossible) { - super(timer, timeRangeProvider, stringMessages, canReplayWhileLiveIsPossible); + RaceTimesInfoProvider raceTimesInfoProvider, boolean canReplayWhileLiveIsPossible, boolean isScreenLargeEnoughToOfferChartSupport) { + super(timer, timeRangeProvider, stringMessages, canReplayWhileLiveIsPossible, isScreenLargeEnoughToOfferChartSupport); this.raceTimesInfoProvider = raceTimesInfoProvider; selectedRace = null; autoAdjustPlayMode = true; 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 4702b1588de..22f8ebbc100 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 @@ -89,7 +89,13 @@ public class TimePanel extends SimplePanel implemen private static ClientResources resources = GWT.create(ClientResources.class); protected static TimePanelCss timePanelCss = TimePanelCssResources.INSTANCE.css(); - public TimePanel(Timer timer, TimeRangeWithZoomProvider timeRangeProvider, StringMessages stringMessages, boolean canReplayWhileLiveIsPossible) { + /** + * @param isScreenLargeEnoughToOfferChartSupport + * if true, the right padding will be set such that the time panel lines up with charts such + * as the competitor chart or the wind chart shown above it + */ + public TimePanel(Timer timer, TimeRangeWithZoomProvider timeRangeProvider, StringMessages stringMessages, + boolean canReplayWhileLiveIsPossible, boolean isScreenLargeEnoughToOfferChartSupport) { this.timer = timer; this.timeRangeProvider = timeRangeProvider; this.stringMessages = stringMessages; @@ -105,8 +111,6 @@ public class TimePanel extends SimplePanel implemen timePanelSliderFlowWrapper = new FlowPanel(); timePanelSlider.setStyleName("timePanelSlider"); timePanelSlider.getElement().getStyle().setPaddingLeft(66, Unit.PX); - // TODO pass this in as a parameter; bug 3345 - boolean isScreenLargeEnoughToOfferChartSupport = true; if (isScreenLargeEnoughToOfferChartSupport) { timePanelSlider.getElement().getStyle().setPaddingRight(66, Unit.PX); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java index 926e3ca3efe..868d60ac7c4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java @@ -151,7 +151,8 @@ public class EmbeddedMapAndWindChartEntryPoint extends AbstractSailingEntryPoint // Use a TimePanel to manage wind chart zoom, although the TimePanel itself is not being displayed; // let the time panel always return to "live" mode. final TimePanel timePanel = new TimePanel( - timer, timeRangeWithZoomProvider, getStringMessages(), /* canReplayWhileLive */ false) { + timer, timeRangeWithZoomProvider, getStringMessages(), /* canReplayWhileLive */ false, + /* isScreenLargeEnoughToOfferChartSupport set to true iff wind chart will be displayed */ showWindChart) { protected boolean isLiveModeToBeMadePossible() { return true; } 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 219d66c3acb..4ba09581c05 100755 --- 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 @@ -6,6 +6,7 @@ import java.util.List; import java.util.UUID; import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; @@ -97,7 +98,7 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { sailingService.getRaceboardData(regattaName, raceName, leaderboardName, leaderboardGroupName, eventId, new AsyncCallback() { @Override public void onSuccess(RaceboardDataDTO result) { - checkUrlParameters(result, canReplayWhileLiveIsPossible, showMapControls); + checkUrlParameters(result, canReplayWhileLiveIsPossible, showMapControls, Document.get().getClientHeight() >= 768); } @Override @@ -116,7 +117,8 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { vp.add(new Label(message)); } - private void checkUrlParameters(RaceboardDataDTO raceboardData, boolean canReplayWhileLiveIsPossible, boolean showMapControls) { + private void checkUrlParameters(RaceboardDataDTO raceboardData, boolean canReplayWhileLiveIsPossible, boolean showMapControls, + boolean isScreenLargeEnoughToOfferChartSupport) { if (!raceboardData.isValidLeaderboard()) { createErrorPage(getStringMessages().noSuchLeaderboard()); return; @@ -148,7 +150,8 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, asyncActionsExecutor, this, singletonList, 5000l /* requestInterval*/); RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, getUserService(), asyncActionsExecutor, raceboardData.getCompetitorAndTheirBoats(), timer, raceSelectionModel, leaderboardName, leaderboardGroupName, eventId, - raceboardViewConfig, RaceBoardEntryPoint.this, getStringMessages(), userAgent, raceTimesInfoProvider, showMapControls); + raceboardViewConfig, RaceBoardEntryPoint.this, getStringMessages(), userAgent, raceTimesInfoProvider, showMapControls, + isScreenLargeEnoughToOfferChartSupport); createRaceBoardInOneScreenMode(raceBoardPanel, raceboardViewConfig); } 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 06c65bf3a1b..3703695f19c 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 @@ -123,13 +123,17 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi * @param eventId * an optional event that can be used for "back"-navigation in case the race board shows a race in the * context of an event; may be null. + * @param isScreenLargeEnoughToOfferChartSupport + * if the screen is large enough to display charts such as the competitor chart or the wind chart, a + * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and + * the charts are created. */ public RaceBoardPanel(SailingServiceAsync sailingService, MediaServiceAsync mediaService, UserService userService, AsyncActionsExecutor asyncActionsExecutor, Map competitorsAndTheirBoats, Timer timer, RaceSelectionProvider theRaceSelectionProvider, String leaderboardName, String leaderboardGroupName, UUID eventId, RaceBoardViewConfiguration raceboardViewConfiguration, ErrorReporter errorReporter, final StringMessages stringMessages, - UserAgentDetails userAgent, RaceTimesInfoProvider raceTimesInfoProvider, boolean showMapControls) { + UserAgentDetails userAgent, RaceTimesInfoProvider raceTimesInfoProvider, boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport) { this.sailingService = sailingService; this.mediaService = mediaService; this.stringMessages = stringMessages; @@ -176,7 +180,7 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi leaderboardPanel.setTitle(stringMessages.leaderboard()); leaderboardPanel.getElement().getStyle().setMarginLeft(6, Unit.PX); leaderboardPanel.getElement().getStyle().setMarginTop(10, Unit.PX); - createOneScreenView(leaderboardName, leaderboardGroupName, eventId, mainPanel, showMapControls, raceMap, userService); // initializes the raceMap field + createOneScreenView(leaderboardName, leaderboardGroupName, eventId, mainPanel, showMapControls, isScreenLargeEnoughToOfferChartSupport, raceMap, userService); // initializes the raceMap field leaderboardPanel.addLeaderboardUpdateListener(this); // in case the URL configuration contains the name of a competitors filter set we try to activate it // FIXME the competitorsFilterSets has now moved to CompetitorSearchTextBox (which should probably be renamed); pass on the parameters to the LeaderboardPanel and see what it does with it @@ -190,7 +194,7 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi } } timePanel = new RaceTimePanel(timer, timeRangeWithZoomModel, stringMessages, raceTimesInfoProvider, - raceboardViewConfiguration.isCanReplayDuringLiveRaces()); + raceboardViewConfiguration.isCanReplayDuringLiveRaces(), isScreenLargeEnoughToOfferChartSupport); timeRangeWithZoomModel.addTimeZoomChangeListener(timePanel); raceTimesInfoProvider.addRaceTimesInfoProviderListener(timePanel); raceSelectionProvider.addRaceSelectionChangeListener(timePanel); @@ -198,17 +202,20 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi } /** - * @param event an optional event; may be null or else can be used to show some context information in - * the {@link GlobalNavigationPanel}. + * @param event + * an optional event; may be null or else can be used to show some context information in + * the {@link GlobalNavigationPanel}. + * @param isScreenLargeEnoughToOfferChartSupport + * if the screen is large enough to display charts such as the competitor chart or the wind chart, a + * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and + * the charts are created. */ private void createOneScreenView(String leaderboardName, String leaderboardGroupName, UUID event, FlowPanel mainPanel, - boolean showMapControls, RaceMap raceMap, UserService userService) { + boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport, RaceMap raceMap, UserService userService) { // create the default leaderboard and select the right race raceTimesInfoProvider.addRaceTimesInfoProviderListener(raceMap); raceMap.onRaceSelectionChange(Collections.singletonList(selectedRaceIdentifier)); List> components = new ArrayList>(); - // TODO pass this in as a parameter; bug 3345 - boolean isScreenLargeEnoughToOfferChartSupport = true; if (isScreenLargeEnoughToOfferChartSupport) { competitorChart = new MultiCompetitorRaceChart(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, timer, timeRangeWithZoomModel, stringMessages, errorReporter, true, true, leaderboardGroupName, leaderboardName); @@ -238,7 +245,7 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi mainPanel.add(componentViewer.getViewerWidget()); } boolean showLeaderboard = getConfiguration().isShowLeaderboard(); - if (Document.get().getClientWidth() <= 1024) { + if (Document.get().getClientWidth() < 1024) { showLeaderboard = false; } setLeaderboardVisible(showLeaderboard); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java index 5df963e9195..6310dcf13cd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java @@ -309,7 +309,7 @@ public class SimulatorMainPanel extends SimplePanel { timer.setTime(windParams.getStartTime().getTime()); int secondsTimeStep = (int) windParams.getTimeStep().asSeconds(); timer.setPlaySpeedFactor(secondsTimeStep); - timePanel = new TimePanel(timer, timeRangeProvider, stringMessages, false); + timePanel = new TimePanel(timer, timeRangeProvider, stringMessages, false, isScreenLargeEnoughToOfferChartSupport); busyIndicator = new SimpleBusyIndicator(false, 0.8f); From f8e3a7f811e3f3e6d57d61531dcb3f4a41000ae7 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 17 Nov 2015 23:35:14 +0100 Subject: [PATCH 012/306] adjusted sizing styles for leaderboard and its competitor search box to allow it to shrink to smaller sizes --- java/com.sap.sailing.gwt.ui/Leaderboard.css | 4 ++-- .../com/sap/sailing/gwt/ui/leaderboard/CompetitorFilter.css | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/Leaderboard.css b/java/com.sap.sailing.gwt.ui/Leaderboard.css index 8ab68acf9b9..ca7ab51f790 100755 --- a/java/com.sap.sailing.gwt.ui/Leaderboard.css +++ b/java/com.sap.sailing.gwt.ui/Leaderboard.css @@ -98,13 +98,13 @@ body, table td, select, button { padding: 49px 0 0 80px; width: 300px; color: #7c7c7c; - background: url(images/important-message.png) left center no-repeat; + background: url(images/important-message.png) left center no-repeat; } .LeaderboardHeader { position: relative; margin: 0 auto; - width: 400px; + width: 100%; text-align: center; font-family: 'Open Sans', Arial, Verdana, sans-serif; font-size: 18px; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilter.css b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilter.css index 3d626453747..5f27df5b24a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilter.css +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilter.css @@ -1,5 +1,6 @@ .competitorFilterContainer { - width: 400px; + max-width: 400px; + width: 100%; height: 2.14em; background-color: #FFFFFF; margin-left: 0px; @@ -22,7 +23,7 @@ .searchInput { font-size: .933333333333333em; height: 2.142857142857143em; - width: 280px; + width: 70%; border: none; background-color: #eaeaea; padding: 0; From f92f1eee394d857cd2a14015090e9929d76b6819 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 18 Nov 2015 00:11:40 +0100 Subject: [PATCH 013/306] pass through the leaderboard settings parameter for showing sail ID and competitor long name columns; use to suppress long name for small initial leaderboard sizes --- .../gwt/ui/test/LeaderboardPanelMock.java | 2 +- .../place/player/AutoPlayController.java | 2 +- .../home/desktop/utils/EventParamUtils.java | 2 +- .../client/RegattaOverviewPanel.java | 2 +- .../ui/leaderboard/LeaderboardEntryPoint.java | 2 +- .../LeaderboardSettingsFactory.java | 24 +++++++++++-------- .../LeaderboardUrlConfigurationDialog.java | 2 +- .../ui/leaderboard/MultiLeaderboardPanel.java | 2 +- .../EditableLeaderboardPanel.java | 2 +- .../gwt/ui/raceboard/RaceBoardPanel.java | 20 +++++++++------- 10 files changed, 33 insertions(+), 27 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java index 4999b3d2c9f..38c0fbdb061 100644 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java @@ -20,7 +20,7 @@ public class LeaderboardPanelMock extends LeaderboardPanel { String leaderboardName, ErrorReporter errorReporter, StringMessages stringMessages) { super(sailingService, new AsyncActionsExecutor(), LeaderboardSettingsFactory.getInstance().createNewDefaultSettings( - /* racesToShow */null, /* namesOfRacesToShow */ null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true), new CompetitorSelectionModel( + /* racesToShow */null, /* namesOfRacesToShow */ null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true, /* showCompetitorSailIdColumn */ true, /* showCompetitorFullNameColumn */ true), new CompetitorSelectionModel( /* hasMultiSelection */true), leaderboardName, errorReporter, stringMessages, new UserAgentDetails("gecko1_8"), /* showRaceDetails */ true); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java index 841a2525cd9..1b9a18e2bd7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java @@ -107,7 +107,7 @@ public class AutoPlayController implements RaceTimesInfoProviderListener { asyncActionsExecutor = new AsyncActionsExecutor(); showWindChart = false; leaderboard = null; - leaderboardSettings = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true); + leaderboardSettings = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true, /* showCompetitorSailIdColumn */ true, /* showCompetitorFullNameColumn */ true); leaderboardTimer = new Timer(PlayModes.Live, /* delayBetweenAutoAdvancesInMilliseconds */1000l); leaderboardTimer.setLivePlayDelayInMillis(delayToLiveInMillis); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/utils/EventParamUtils.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/utils/EventParamUtils.java index d10d5917c86..e2c3f612111 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/utils/EventParamUtils.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/utils/EventParamUtils.java @@ -108,7 +108,7 @@ public class EventParamUtils { .createNewDefaultSettings(null, null, /* overallDetails */overallDetails, null, /* autoExpandFirstRace */false, refreshIntervalMillis, numberOfLastRacesToShow, - raceColumnSelectionStrategy); + raceColumnSelectionStrategy, /* showCompetitorSailIdColumns */ true, /*showCompetitorFullNameColumn*/ true); } return result; } 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 4d4de5230a4..fc2180d3170 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 @@ -237,7 +237,7 @@ public class RegattaOverviewPanel extends SimplePanel { if (leaderboardsTabPanel != null) { if (showLeaderboard) { final CompetitorSelectionModel competitorSelectionProvider = new CompetitorSelectionModel(/* hasMultiSelection */ true); - final LeaderboardSettings leaderboardSettings = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true); + final LeaderboardSettings leaderboardSettings = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true, /* showCompetitorSailIdColumn */ true, /* showCompetitorFullNameColumn */ true); sailingService.getLeaderboardsByEvent(eventDTO, new MarkedAsyncCallback>( new AsyncCallback>() { @Override 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 1f54ca0315f..79f24c91d28 100755 --- 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 @@ -258,7 +258,7 @@ public class LeaderboardEntryPoint extends AbstractSailingEntryPoint { result = LeaderboardSettingsFactory.getInstance().createNewDefaultSettings(null, null, /* overallDetails */ overallDetails, null, /* autoExpandFirstRace */false, refreshIntervalMillis, numberOfLastRacesToShow, - raceColumnSelectionStrategy); + raceColumnSelectionStrategy, /* showCompetitorSailIdColumns */ true, /*showCompetitorFullNameColumn*/ true); } return result; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsFactory.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsFactory.java index e2d4275010b..8cdeefda0ea 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsFactory.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardSettingsFactory.java @@ -78,11 +78,11 @@ public class LeaderboardSettingsFactory { /* refresh interval */ null, /* name of race to sort */ nameOfRaceToSort, /* ascending */ true, /* updateUponPlayStateChange */ true, raceColumnSelection.getType(), /*showAddedScores*/ false, /*showOverallRacesCompleted*/ false, - /*showCompetitorSailIdColumns*/showCompetitorSailIdColumn, /*showCompetitorFullNameColumn*/showCompetitorNameColumn); + showCompetitorSailIdColumn, showCompetitorNameColumn); break; case Replay: settings = createNewDefaultSettings(namesOfRaceColumnsToShow, namesOfRacesToShow, nameOfRaceToSort, /* autoExpandFirstRace */ - nameOfRaceColumnToShow != null, showRegattaRank); + nameOfRaceColumnToShow != null, showRegattaRank, showCompetitorSailIdColumn, showCompetitorNameColumn); break; } return settings; @@ -127,28 +127,32 @@ public class LeaderboardSettingsFactory { * such a meta leaderboard exists; if multiple such meta leaderboards exist, they are all shown */ public LeaderboardSettings createNewDefaultSettings(List namesOfRaceColumnsToShow, - List namesOfRacesToShow, String nameOfRaceToSort, boolean autoExpandPreSelectedRace, Boolean showRegattaRank) { + List namesOfRacesToShow, String nameOfRaceToSort, boolean autoExpandPreSelectedRace, Boolean showRegattaRank, + boolean showCompetitorSailIdColumn, boolean showCompetitorFullNameColumn) { final List overallDetails = createOverallDetailsForShowRegattaRank(showRegattaRank); return createNewDefaultSettings(namesOfRaceColumnsToShow, namesOfRacesToShow, overallDetails, - nameOfRaceToSort, autoExpandPreSelectedRace, /* refreshIntervalMillis */ null); + nameOfRaceToSort, autoExpandPreSelectedRace, /* refreshIntervalMillis */ null, + showCompetitorSailIdColumn, showCompetitorFullNameColumn); } /** - * Like {@link #createNewDefaultSettings(List, List, String, boolean, boolean)}, only that an additional refresh interval for auto-refresh + * Like {@link #createNewDefaultSettings(List, List, String, boolean, boolean, boolean, boolean)}, only that an additional refresh interval for auto-refresh * may be specified; if null, no auto-refresh shall be performed */ - public LeaderboardSettings createNewDefaultSettings(List namesOfRaceColumnsToShow, + private LeaderboardSettings createNewDefaultSettings(List namesOfRaceColumnsToShow, List namesOfRacesToShow, List overallDetailsToShow, String nameOfRaceToSort, - boolean autoExpandPreSelectedRace, Long refreshIntervalMillis) { + boolean autoExpandPreSelectedRace, Long refreshIntervalMillis, boolean showCompetitorSailIdColumns, boolean showCompetitorFullNameColumn) { return createNewDefaultSettings(namesOfRaceColumnsToShow, namesOfRacesToShow, overallDetailsToShow, nameOfRaceToSort, autoExpandPreSelectedRace, refreshIntervalMillis, - /* numberOfLastRacesToShow */null, /* raceColumnSelectionStrategy */ RaceColumnSelectionStrategies.EXPLICIT); + /* numberOfLastRacesToShow */null, /* raceColumnSelectionStrategy */ RaceColumnSelectionStrategies.EXPLICIT, + showCompetitorSailIdColumns, showCompetitorFullNameColumn); } public LeaderboardSettings createNewDefaultSettings(List namesOfRaceColumnsToShow, List namesOfRacesToShow, List overallDetailsToShow, String nameOfRaceToSort, boolean autoExpandPreSelectedRace, Long refreshIntervalMillis, Integer numberOfLastRacesToShow, - RaceColumnSelectionStrategies raceColumnSelectionStrategy) { + RaceColumnSelectionStrategies raceColumnSelectionStrategy, boolean showCompetitorSailIdColumns, + boolean showCompetitorFullNameColumn) { if (namesOfRaceColumnsToShow != null && namesOfRacesToShow != null) { throw new IllegalArgumentException("Can specify race columns either by column or by race name, not both"); } @@ -168,7 +172,7 @@ public class LeaderboardSettingsFactory { autoExpandPreSelectedRace, refreshIntervalMillis, /* sort by column */ nameOfRaceToSort, /* ascending */ true, /* updateUponPlayStateChange */ true, raceColumnSelectionStrategy, /*showAddedScores*/ false, /*showOverallRacesCompleted*/ false, - /*showCompetitorSailIdColumns*/true, /*showCompetitorFullNameColumn*/true); + showCompetitorSailIdColumns, showCompetitorFullNameColumn); } public LeaderboardSettings mergeLeaderboardSettings(LeaderboardSettings settingsWithRaceSelection, LeaderboardSettings settingsWithDetails) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardUrlConfigurationDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardUrlConfigurationDialog.java index b3cc3e30c4f..dec01ccef93 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardUrlConfigurationDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardUrlConfigurationDialog.java @@ -106,7 +106,7 @@ public class LeaderboardUrlConfigurationDialog extends SettingsDialog(new ArrayList()); 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 3703695f19c..6887c7aade4 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 @@ -176,11 +176,12 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi raceMap.getLeftHeaderPanel().add(raceInformationHeader); raceMap.getRightHeaderPanel().add(regattaAndRaceTimeInformationHeader); - leaderboardPanel = createLeaderboardPanel(leaderboardName, leaderboardGroupName, competitorSearchTextBox); + boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard = Document.get().getClientWidth() >= 1024; + leaderboardPanel = createLeaderboardPanel(leaderboardName, leaderboardGroupName, competitorSearchTextBox, isScreenLargeEnoughToInitiallyDisplayLeaderboard); leaderboardPanel.setTitle(stringMessages.leaderboard()); leaderboardPanel.getElement().getStyle().setMarginLeft(6, Unit.PX); leaderboardPanel.getElement().getStyle().setMarginTop(10, Unit.PX); - createOneScreenView(leaderboardName, leaderboardGroupName, eventId, mainPanel, showMapControls, isScreenLargeEnoughToOfferChartSupport, raceMap, userService); // initializes the raceMap field + createOneScreenView(leaderboardName, leaderboardGroupName, eventId, mainPanel, showMapControls, isScreenLargeEnoughToOfferChartSupport, isScreenLargeEnoughToInitiallyDisplayLeaderboard, raceMap, userService); // initializes the raceMap field leaderboardPanel.addLeaderboardUpdateListener(this); // in case the URL configuration contains the name of a competitors filter set we try to activate it // FIXME the competitorsFilterSets has now moved to CompetitorSearchTextBox (which should probably be renamed); pass on the parameters to the LeaderboardPanel and see what it does with it @@ -209,9 +210,10 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi * if the screen is large enough to display charts such as the competitor chart or the wind chart, a * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and * the charts are created. + * @param isScreenLargeEnoughToInitiallyDisplayLeaderboard TODO */ private void createOneScreenView(String leaderboardName, String leaderboardGroupName, UUID event, FlowPanel mainPanel, - boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport, RaceMap raceMap, UserService userService) { + boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport, boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard, RaceMap raceMap, UserService userService) { // create the default leaderboard and select the right race raceTimesInfoProvider.addRaceTimesInfoProviderListener(raceMap); raceMap.onRaceSelectionChange(Collections.singletonList(selectedRaceIdentifier)); @@ -244,10 +246,7 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi for (ComponentViewer componentViewer : componentViewers) { mainPanel.add(componentViewer.getViewerWidget()); } - boolean showLeaderboard = getConfiguration().isShowLeaderboard(); - if (Document.get().getClientWidth() < 1024) { - showLeaderboard = false; - } + boolean showLeaderboard = getConfiguration().isShowLeaderboard() && isScreenLargeEnoughToInitiallyDisplayLeaderboard; setLeaderboardVisible(showLeaderboard); if (isScreenLargeEnoughToOfferChartSupport) { setWindChartVisible(getConfiguration().isShowWindChart()); @@ -271,13 +270,16 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi } } - private LeaderboardPanel createLeaderboardPanel(String leaderboardName, String leaderboardGroupName, CompetitorFilterPanel competitorSearchTextBox) { + private LeaderboardPanel createLeaderboardPanel(String leaderboardName, String leaderboardGroupName, + CompetitorFilterPanel competitorSearchTextBox, boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard) { LeaderboardSettings leaderBoardSettings = LeaderboardSettingsFactory.getInstance() .createNewSettingsForPlayMode(timer.getPlayMode(), /* nameOfRaceToSort */ selectedRaceIdentifier.getRaceName(), /* nameOfRaceColumnToShow */ null, /* nameOfRaceToShow */ selectedRaceIdentifier.getRaceName(), new ExplicitRaceColumnSelectionWithPreselectedRace(selectedRaceIdentifier), /* showRegattaRank */ false, - /*showCompetitorSailIdColumn*/true, /*showCompetitorFullNameColumn*/true); + /*showCompetitorSailIdColumn*/true, + /* don't showCompetitorFullNameColumn in case screen is so small that we don't + * even display the leaderboard initially */ isScreenLargeEnoughToInitiallyDisplayLeaderboard); return new LeaderboardPanel(sailingService, asyncActionsExecutor, leaderBoardSettings, selectedRaceIdentifier != null, selectedRaceIdentifier, competitorSelectionProvider, timer, leaderboardGroupName, leaderboardName, errorReporter, stringMessages, userAgent, /* showRaceDetails */ true, competitorSearchTextBox, From 1f20c354951a3b6b41613b8514ee903fc04241b8 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 18 Nov 2015 00:19:53 +0100 Subject: [PATCH 014/306] added comment --- .../com/sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 4ba09581c05..3e95fa2d33a 100755 --- 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 @@ -98,7 +98,8 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { sailingService.getRaceboardData(regattaName, raceName, leaderboardName, leaderboardGroupName, eventId, new AsyncCallback() { @Override public void onSuccess(RaceboardDataDTO result) { - checkUrlParameters(result, canReplayWhileLiveIsPossible, showMapControls, Document.get().getClientHeight() >= 768); + checkUrlParameters(result, canReplayWhileLiveIsPossible, showMapControls, + /* isScreenLargeEnoughToOfferChartSupport */ Document.get().getClientHeight() >= 600); } @Override From 62215a56be07afe13473425883a22ba91088268f Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 18 Nov 2015 07:19:22 +0100 Subject: [PATCH 015/306] fixed compile issue by using full timei panel extension in simulator --- .../com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java index 6310dcf13cd..a39b7e3df7d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java @@ -309,7 +309,9 @@ public class SimulatorMainPanel extends SimplePanel { timer.setTime(windParams.getStartTime().getTime()); int secondsTimeStep = (int) windParams.getTimeStep().asSeconds(); timer.setPlaySpeedFactor(secondsTimeStep); - timePanel = new TimePanel(timer, timeRangeProvider, stringMessages, false, isScreenLargeEnoughToOfferChartSupport); + timePanel = new TimePanel(timer, timeRangeProvider, stringMessages, false, + /* isScreenLargeEnoughToOfferChartSupport: no wind or competitor chart is shown; use full horizontal + * extension of time panel */ false); busyIndicator = new SimpleBusyIndicator(false, 0.8f); From 75f5d1388a1d5550d8a23ccc4452152d5b2306ca Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 18 Nov 2015 16:18:24 +0100 Subject: [PATCH 016/306] added release notes entry about the specifics of branch 3345 --- .../sailing/gwt/home/solutions/SailingAnalyticsNotes.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html index 547230f6227..5bbd603fe5a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html @@ -17,6 +17,13 @@
  • Regattas can now be filtered by boat category in the smartphone page. The filter will be shown for events with multiple regattas if there is more than on boat category associated with the event.
  • +
  • The race viewer (RaceBoard.html) now intelligently chooses a layout that suits the screen size. + When only little horizontal space is available, only the sail number and nationality but not the + competitor's long name is displayed by default. When vertical space is scarce, competitor and wind + charts are disabled and more horizontal space can be given to the time slider at the bottom. The + leaderboard section can then be scaled to smaller widths and will reduce the column widths better + than in the past. +
  • October 2015
    From 2f93a09e89b639e018d7c21de147280bdd54667e Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 19 Nov 2015 15:10:03 +0100 Subject: [PATCH 017/306] Enabled mobile race viewer link and adjusted parameters --- .../gwt/home/mobile/places/event/AbstractEventActivity.java | 2 -- .../com/sap/sailing/gwt/home/shared/ExperimentalFeatures.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java index d0867a28588..4edafe69a6b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/AbstractEventActivity.java @@ -44,7 +44,6 @@ import com.sap.sailing.gwt.home.shared.places.fakeseries.SeriesDefaultPlace; import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.refresh.ErrorAndBusyClientFactory; -import com.sap.sailing.gwt.ui.raceboard.RaceBoardViewConfiguration; import com.sap.sailing.gwt.ui.shared.util.NullSafeComparableComparator; public abstract class AbstractEventActivity extends AbstractActivity implements Presenter { @@ -248,7 +247,6 @@ public abstract class AbstractEventActivity ex } linkParams.put("raceName", trackedRaceName); linkParams.put("regattaName", regattaName); - linkParams.put(RaceBoardViewConfiguration.PARAM_VIEW_MODE, "simple"); // TODO this must only be forwarded if there is a logged-on user // linkParams.put(RaceBoardViewConfiguration.PARAM_CAN_REPLAY_DURING_LIVE_RACES, "true"); return linkParams; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/ExperimentalFeatures.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/ExperimentalFeatures.java index dcfbd043171..53366e8b6d0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/ExperimentalFeatures.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/ExperimentalFeatures.java @@ -54,7 +54,7 @@ public class ExperimentalFeatures { /** * Enables/disables the link to the race board on mobile races view (competition format), in case of a tracked race */ - public static final boolean ENABLE_RACE_VIEWER_LINK_ON_MOBILE = false; + public static final boolean ENABLE_RACE_VIEWER_LINK_ON_MOBILE = true; /** * Provide selection to filter regattas by boat category on mobile multiregatta event overview */ From 6267df950425279539545f24d845b9414db5f9e1 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 20 Nov 2015 10:46:54 +0100 Subject: [PATCH 018/306] renamed interface for entity identity check --- .../ui/adminconsole/EventListComposite.java | 16 +++++------ .../gwt/ui/client/RegattaSelectionModel.java | 12 ++++----- .../celltable/EntityIdentityComparator.java | 27 +++++++++++++++++++ .../client/celltable/HasEqualIdentity.java | 8 ------ .../RefreshableMultiSelectionModel.java | 14 +++++----- .../celltable/RefreshableSelectionModel.java | 4 +-- .../RefreshableSingleSelectionModel.java | 14 +++++----- 7 files changed, 57 insertions(+), 38 deletions(-) create mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java delete mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/HasEqualIdentity.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 0ce31f366d7..669cea70472 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -48,7 +48,7 @@ import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.celltable.HasEqualIdentity; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -147,10 +147,10 @@ public class EventListComposite extends Composite implements EventsRefresher, Le @SuppressWarnings("unchecked") RefreshableMultiSelectionModel multiSelectionModel = (RefreshableMultiSelectionModel) eventTable.getSelectionModel(); - multiSelectionModel.setHasEqualIdentity(new HasEqualIdentity() { + multiSelectionModel.setHasEqualIdentity(new EntityIdentityComparator() { @Override - public boolean compare(EventDTO o1, EventDTO o2) { - return o1.id.equals(o2.id) ? true : false; + public boolean representSameEntity(EventDTO dto1, EventDTO dto2) { + return dto1.id.equals(dto2.id) ? true : false; } }); eventSelectionModel = multiSelectionModel; @@ -380,19 +380,19 @@ public class EventListComposite extends Composite implements EventsRefresher, Le ListHandler result = new ListHandler(eventRecords); result.setComparator(eventNameColumn, new Comparator() { @Override - public int compare(EventDTO e1, EventDTO e2) { + public int representSameEntity(EventDTO e1, EventDTO e2) { return new NaturalComparator().compare(e1.getName(), e2.getName()); } }); result.setComparator(venueNameColumn, new Comparator() { @Override - public int compare(EventDTO e1, EventDTO e2) { + public int representSameEntity(EventDTO e1, EventDTO e2) { return new NaturalComparator().compare(e1.venue.getName(), e2.venue.getName()); } }); result.setComparator(startEndDateColumn, new Comparator() { @Override - public int compare(EventDTO e1, EventDTO e2) { + public int representSameEntity(EventDTO e1, EventDTO e2) { int result; if(e1.startDate != null && e2.startDate != null) { result = e2.startDate.compareTo(e1.startDate); @@ -408,7 +408,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le }); result.setComparator(isPublicColumn, new Comparator() { @Override - public int compare(EventDTO e1, EventDTO e2) { + public int representSameEntity(EventDTO e1, EventDTO e2) { return e1.isPublic == e2.isPublic ? 0 : e1.isPublic ? 1 : -1; } }); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java index f7e3aed53c3..59493aa7c26 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java @@ -9,7 +9,7 @@ import java.util.List; import java.util.Set; import com.sap.sailing.domain.common.RegattaIdentifier; -import com.sap.sse.gwt.client.celltable.HasEqualIdentity; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; public class RegattaSelectionModel implements RegattaSelectionProvider, RefreshableSelectionModel { @@ -99,11 +99,11 @@ public class RegattaSelectionModel implements RegattaSelectionProvider, Refresha * method works the same way as o1.eqauls(o2). */ @Override - public HasEqualIdentity getHasEqualIdentity() { - return new HasEqualIdentity() { + public EntityIdentityComparator getHasEqualIdentity() { + return new EntityIdentityComparator() { @Override - public boolean compare(RegattaIdentifier o1, RegattaIdentifier o2) { - return o1.equals(o2); + public boolean representSameEntity(RegattaIdentifier dto1, RegattaIdentifier dto2) { + return dto1.equals(dto2); } }; } @@ -113,7 +113,7 @@ public class RegattaSelectionModel implements RegattaSelectionProvider, Refresha * compare RegattaIdentifier objects is to use the equals method. */ @Override - public void setHasEqualIdentity(HasEqualIdentity comp) { + public void setHasEqualIdentity(EntityIdentityComparator comp) { //TODO } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java new file mode 100644 index 00000000000..2c36b7e639a --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java @@ -0,0 +1,27 @@ +package com.sap.sse.gwt.client.celltable; + +/** + * An entity can change its state over time while remaining the same. Objects such as DTOs (data transfer objects) + * represent the state of an entity at a given point in time, so two such DTOs may represent different states of the + * same entity. These copied may not be equal due to their different state. So, e.g., a UI that needs to decide whether + * a refresh is needed would rely on this regular definition of state-based equality. + *

    + * + * However, different, non-equal objects may still represent the same entity. This equivalence can become important, e.g., for + * selection management where in a UI widget a new and different copy of the same entity is being displayed and an old copy + * was in the selection.

    + * + * Implementors of this interface are expected to define under which conditions two objects represent the same + * entity. This can, e.g., be decided based on an ID field if the object type offers one. + * + * @author Lukas Furmanek + * + * @param type of the entity representation, such as a DTO class + */ +public interface EntityIdentityComparator { + /** + * Compares the objects and returns true if the objects represent the same entity. + * Note that this does not imply that they are also {@link #equals(Object) equal}. + */ + public boolean representSameEntity(T dto1, T dto2); +} \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/HasEqualIdentity.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/HasEqualIdentity.java deleted file mode 100644 index f144c9ebcb0..00000000000 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/HasEqualIdentity.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.sap.sse.gwt.client.celltable; - -public interface HasEqualIdentity { - /** - * Compares the Objects and return true , if the objects have the same Identity. - */ - public boolean compare(T o1, T o2); -} \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index ea3acd394f2..43d2f7691ec 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -6,42 +6,42 @@ import com.google.gwt.view.client.MultiSelectionModel; import com.google.gwt.view.client.ProvidesKey; public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel { - private HasEqualIdentity comp; + private EntityIdentityComparator comp; public RefreshableMultiSelectionModel() { super(); comp = null; } - public RefreshableMultiSelectionModel(HasEqualIdentity comp) { + public RefreshableMultiSelectionModel(EntityIdentityComparator comp) { super(); this.comp = comp; } - public RefreshableMultiSelectionModel(ProvidesKey keyProvider, HasEqualIdentity comp) { + public RefreshableMultiSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { super(keyProvider); this.comp = comp; } @Override - public HasEqualIdentity getHasEqualIdentity() { + public EntityIdentityComparator getHasEqualIdentity() { return comp; } @Override - public void setHasEqualIdentity(HasEqualIdentity comp) { + public void setHasEqualIdentity(EntityIdentityComparator comp) { this.comp = comp; } @Override public void refreshSelectionModel(Iterable newObjects) { Set selectedSet = getSelectedSet(); - HasEqualIdentity comp = getHasEqualIdentity(); + EntityIdentityComparator comp = getHasEqualIdentity(); clear(); if (comp != null) { for(T it : newObjects) { for(T selected : selectedSet) { - if(comp.compare(selected, it)) { + if(comp.representSameEntity(selected, it)) { setSelected(it, true); } else { setSelected(it, false); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java index b54fbe8210a..7a9700f2c3e 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java @@ -7,7 +7,7 @@ public interface RefreshableSelectionModel { * @return Returns an instance of HasEqualIdentity<T>, to compare objects from the type * <T>. */ - public HasEqualIdentity getHasEqualIdentity(); + public EntityIdentityComparator getHasEqualIdentity(); /** * Sets the HasEqualIdentity<T> field, to compare objects from the type <T>. @@ -15,7 +15,7 @@ public interface RefreshableSelectionModel { * @param comp * When you set null, the T.eqauls() will be used. */ - public void setHasEqualIdentity(HasEqualIdentity comp); + public void setHasEqualIdentity(EntityIdentityComparator comp); public Set getSelectedSet(); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index a6be572e965..c3f32248685 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -6,42 +6,42 @@ import com.google.gwt.view.client.ProvidesKey; import com.google.gwt.view.client.SingleSelectionModel; public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel { - private HasEqualIdentity comp; + private EntityIdentityComparator comp; public RefreshableSingleSelectionModel() { super(); comp = null; } - public RefreshableSingleSelectionModel(HasEqualIdentity comp) { + public RefreshableSingleSelectionModel(EntityIdentityComparator comp) { super(); this.comp=comp; } - public RefreshableSingleSelectionModel(ProvidesKey keyProvider, HasEqualIdentity comp) { + public RefreshableSingleSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { super(keyProvider); this.comp =comp; } @Override - public HasEqualIdentity getHasEqualIdentity() { + public EntityIdentityComparator getHasEqualIdentity() { return comp; } @Override - public void setHasEqualIdentity(HasEqualIdentity comp) { + public void setHasEqualIdentity(EntityIdentityComparator comp) { this.comp = comp; } @Override public void refreshSelectionModel(Iterable newObjects) { Set selectedSet = getSelectedSet(); - HasEqualIdentity comp = getHasEqualIdentity(); + EntityIdentityComparator comp = getHasEqualIdentity(); clear(); if (comp != null) { for(T it : newObjects) { for(T selected : selectedSet) { - if(comp.compare(selected, it)) { + if(comp.representSameEntity(selected, it)) { setSelected(it, true); } else { setSelected(it, false); From 3a7cf529bfb2452d814c29d8ea82ec79f1ce450b Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 20 Nov 2015 10:53:30 +0100 Subject: [PATCH 019/306] reverted broken Eclipse refactoring regarding renaming of compare method --- .../gwt/ui/adminconsole/EventListComposite.java | 10 +++++----- .../sailing/gwt/ui/client/RegattaSelectionModel.java | 2 +- .../gwt/client/celltable/EntityIdentityComparator.java | 2 +- .../celltable/RefreshableMultiSelectionModel.java | 2 +- .../celltable/RefreshableSingleSelectionModel.java | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 669cea70472..0dbdd43c553 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -149,7 +149,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le RefreshableMultiSelectionModel multiSelectionModel = (RefreshableMultiSelectionModel) eventTable.getSelectionModel(); multiSelectionModel.setHasEqualIdentity(new EntityIdentityComparator() { @Override - public boolean representSameEntity(EventDTO dto1, EventDTO dto2) { + public boolean compare(EventDTO dto1, EventDTO dto2) { return dto1.id.equals(dto2.id) ? true : false; } }); @@ -380,19 +380,19 @@ public class EventListComposite extends Composite implements EventsRefresher, Le ListHandler result = new ListHandler(eventRecords); result.setComparator(eventNameColumn, new Comparator() { @Override - public int representSameEntity(EventDTO e1, EventDTO e2) { + public int compare(EventDTO e1, EventDTO e2) { return new NaturalComparator().compare(e1.getName(), e2.getName()); } }); result.setComparator(venueNameColumn, new Comparator() { @Override - public int representSameEntity(EventDTO e1, EventDTO e2) { + public int compare(EventDTO e1, EventDTO e2) { return new NaturalComparator().compare(e1.venue.getName(), e2.venue.getName()); } }); result.setComparator(startEndDateColumn, new Comparator() { @Override - public int representSameEntity(EventDTO e1, EventDTO e2) { + public int compare(EventDTO e1, EventDTO e2) { int result; if(e1.startDate != null && e2.startDate != null) { result = e2.startDate.compareTo(e1.startDate); @@ -408,7 +408,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le }); result.setComparator(isPublicColumn, new Comparator() { @Override - public int representSameEntity(EventDTO e1, EventDTO e2) { + public int compare(EventDTO e1, EventDTO e2) { return e1.isPublic == e2.isPublic ? 0 : e1.isPublic ? 1 : -1; } }); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java index 59493aa7c26..10823401224 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java @@ -102,7 +102,7 @@ public class RegattaSelectionModel implements RegattaSelectionProvider, Refresha public EntityIdentityComparator getHasEqualIdentity() { return new EntityIdentityComparator() { @Override - public boolean representSameEntity(RegattaIdentifier dto1, RegattaIdentifier dto2) { + public boolean compare(RegattaIdentifier dto1, RegattaIdentifier dto2) { return dto1.equals(dto2); } }; diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java index 2c36b7e639a..8c9ac1a8548 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java @@ -23,5 +23,5 @@ public interface EntityIdentityComparator { * Compares the objects and returns true if the objects represent the same entity. * Note that this does not imply that they are also {@link #equals(Object) equal}. */ - public boolean representSameEntity(T dto1, T dto2); + public boolean compare(T dto1, T dto2); } \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 43d2f7691ec..1ba1bb8cf74 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -41,7 +41,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im if (comp != null) { for(T it : newObjects) { for(T selected : selectedSet) { - if(comp.representSameEntity(selected, it)) { + if(comp.compare(selected, it)) { setSelected(it, true); } else { setSelected(it, false); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index c3f32248685..e1694c58544 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -41,7 +41,7 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel if (comp != null) { for(T it : newObjects) { for(T selected : selectedSet) { - if(comp.representSameEntity(selected, it)) { + if(comp.compare(selected, it)) { setSelected(it, true); } else { setSelected(it, false); From 072d12c4caf10eb326927999458c311484fc36f0 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 20 Nov 2015 11:00:54 +0100 Subject: [PATCH 020/306] completed rename of method in EntityIdentityComparator --- .../gwt/ui/adminconsole/EventListComposite.java | 2 +- .../sailing/gwt/ui/client/RegattaSelectionModel.java | 2 +- .../client/celltable/EntityIdentityComparator.java | 2 +- .../celltable/RefreshableMultiSelectionModel.java | 12 ++++++------ .../celltable/RefreshableSingleSelectionModel.java | 12 ++++++------ 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 0dbdd43c553..24e4de1e52f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -149,7 +149,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le RefreshableMultiSelectionModel multiSelectionModel = (RefreshableMultiSelectionModel) eventTable.getSelectionModel(); multiSelectionModel.setHasEqualIdentity(new EntityIdentityComparator() { @Override - public boolean compare(EventDTO dto1, EventDTO dto2) { + public boolean representSameEntity(EventDTO dto1, EventDTO dto2) { return dto1.id.equals(dto2.id) ? true : false; } }); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java index 10823401224..59493aa7c26 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java @@ -102,7 +102,7 @@ public class RegattaSelectionModel implements RegattaSelectionProvider, Refresha public EntityIdentityComparator getHasEqualIdentity() { return new EntityIdentityComparator() { @Override - public boolean compare(RegattaIdentifier dto1, RegattaIdentifier dto2) { + public boolean representSameEntity(RegattaIdentifier dto1, RegattaIdentifier dto2) { return dto1.equals(dto2); } }; diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java index 8c9ac1a8548..2c36b7e639a 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/EntityIdentityComparator.java @@ -23,5 +23,5 @@ public interface EntityIdentityComparator { * Compares the objects and returns true if the objects represent the same entity. * Note that this does not imply that they are also {@link #equals(Object) equal}. */ - public boolean compare(T dto1, T dto2); + public boolean representSameEntity(T dto1, T dto2); } \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 1ba1bb8cf74..0f2eba71845 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -39,9 +39,9 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im EntityIdentityComparator comp = getHasEqualIdentity(); clear(); if (comp != null) { - for(T it : newObjects) { - for(T selected : selectedSet) { - if(comp.compare(selected, it)) { + for (T it : newObjects) { + for (T selected : selectedSet) { + if (comp.representSameEntity(selected, it)) { setSelected(it, true); } else { setSelected(it, false); @@ -49,9 +49,9 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im } } } else { - for(T it : newObjects) { - for(T selected : selectedSet) { - if(selected.equals(it)) { + for (T it : newObjects) { + for (T selected : selectedSet) { + if (selected.equals(it)) { setSelected(it, true); } else { setSelected(it, false); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index e1694c58544..feff69d4c11 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -39,9 +39,9 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel EntityIdentityComparator comp = getHasEqualIdentity(); clear(); if (comp != null) { - for(T it : newObjects) { - for(T selected : selectedSet) { - if(comp.compare(selected, it)) { + for (T it : newObjects) { + for (T selected : selectedSet) { + if (comp.representSameEntity(selected, it)) { setSelected(it, true); } else { setSelected(it, false); @@ -49,9 +49,9 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel } } } else { - for(T it : newObjects) { - for(T selected : selectedSet) { - if(selected.equals(it)) { + for (T it : newObjects) { + for (T selected : selectedSet) { + if (selected.equals(it)) { setSelected(it, true); } else { setSelected(it, false); From 632364416e4abb14e85b586434b6a98734a766ac Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 20 Nov 2015 13:49:54 +0100 Subject: [PATCH 021/306] some minor refactorings and changes that don't yet compile; Lukas to continue --- .../adminconsole/RegattaManagementPanel.java | 2 +- .../gwt/ui/client/RegattaSelectionModel.java | 4 +-- .../RefreshableMultiSelectionModel.java | 4 +-- .../celltable/RefreshableSelectionModel.java | 31 +++++++++---------- .../RefreshableSingleSelectionModel.java | 31 +++++-------------- 5 files changed, 28 insertions(+), 44 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java index 236bce167ba..02497fabd54 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java @@ -225,7 +225,7 @@ public class RegattaManagementPanel extends SimplePanel implements RegattasDispl @Override public void onRegattaSelectionChange(List selectedRegattas) { final RegattaIdentifier selectedRegatta; - if (selectedRegattas.size() == 1 && selectedRegattas.iterator().hasNext()) { + if (selectedRegattas.size() == 1) { selectedRegatta = selectedRegattas.iterator().next(); if (selectedRegatta != null && regattaListComposite.getAllRegattas() != null) { for (RegattaDTO regattaDTO : regattaListComposite.getAllRegattas()) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java index 59493aa7c26..815785ae0c7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java @@ -99,7 +99,7 @@ public class RegattaSelectionModel implements RegattaSelectionProvider, Refresha * method works the same way as o1.eqauls(o2). */ @Override - public EntityIdentityComparator getHasEqualIdentity() { + public EntityIdentityComparator getEntityIdentityComparator() { return new EntityIdentityComparator() { @Override public boolean representSameEntity(RegattaIdentifier dto1, RegattaIdentifier dto2) { @@ -114,7 +114,7 @@ public class RegattaSelectionModel implements RegattaSelectionProvider, Refresha */ @Override public void setHasEqualIdentity(EntityIdentityComparator comp) { - //TODO + //TODO remove } @Override diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 0f2eba71845..4886937d8d0 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -24,7 +24,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im } @Override - public EntityIdentityComparator getHasEqualIdentity() { + public EntityIdentityComparator getEntityIdentityComparator() { return comp; } @@ -36,7 +36,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im @Override public void refreshSelectionModel(Iterable newObjects) { Set selectedSet = getSelectedSet(); - EntityIdentityComparator comp = getHasEqualIdentity(); + EntityIdentityComparator comp = getEntityIdentityComparator(); clear(); if (comp != null) { for (T it : newObjects) { diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java index 7a9700f2c3e..a4c94f3deae 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java @@ -1,35 +1,34 @@ package com.sap.sse.gwt.client.celltable; -import java.util.Set; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SetSelectionModel; -public interface RefreshableSelectionModel { +public interface RefreshableSelectionModel extends SetSelectionModel { /** - * @return Returns an instance of HasEqualIdentity<T>, to compare objects from the type + * @return Returns an instance of {@link EntityIdentityComparator}, to compare objects from the type * <T>. */ - public EntityIdentityComparator getHasEqualIdentity(); + public EntityIdentityComparator getEntityIdentityComparator(); /** - * Sets the HasEqualIdentity<T> field, to compare objects from the type <T>. + * Sets the {@link EntityIdentityComparator} field, to compare objects from the type <T>. * * @param comp * When you set null, the T.eqauls() will be used. */ + // TODO remove this setter and pass the comparator to the constructor of the implementing class(es) public void setHasEqualIdentity(EntityIdentityComparator comp); - public Set getSelectedSet(); - - public void clear(); - - public void setSelected(T item, boolean selected); - /** - * Refreshes the RefreshableSelectionModel<T> with the newObjects. If the selected - * objects are according to HasEqualIdentity<T>.compare() are the same, the selection will be - * reselected. If the object have no HasEqualIdentity<T>, this method will use the - * <T>.eqauls() method to compare. + * Refreshes the {@link RefreshableSelectionModel} with the newObjects. All objects from the current + * selection that {@link EntityIdentityComparator#representSameEntity(Object, Object) represent the same entity} as + * an object from newObjects will be reselected. All others are de-selected. If this selection model + * has no {@link EntityIdentityComparator} set, this method will use the {@link #equals(Object)} method to compare. + *

    + * + * TODO what happens with objects that were in the selection but are not in newObjects? + * TODO what about triggering an {@link SelectionChangeEvent.Handler#onSelectionChange(SelectionChangeEvent)}, e.g., using {@link AbstractSelectionModel#scheduleSelectionChangeEvent} * - * @param newObjects */ public void refreshSelectionModel(Iterable newObjects); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index feff69d4c11..5965683d480 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -24,39 +24,24 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel } @Override - public EntityIdentityComparator getHasEqualIdentity() { + public EntityIdentityComparator getEntityIdentityComparator() { return comp; } @Override public void setHasEqualIdentity(EntityIdentityComparator comp) { - this.comp = comp; + this.comp = comp; + // TODO remove } @Override public void refreshSelectionModel(Iterable newObjects) { - Set selectedSet = getSelectedSet(); - EntityIdentityComparator comp = getHasEqualIdentity(); + final Set selectedSet = getSelectedSet(); + final EntityIdentityComparator comp = getEntityIdentityComparator(); clear(); - if (comp != null) { - for (T it : newObjects) { - for (T selected : selectedSet) { - if (comp.representSameEntity(selected, it)) { - setSelected(it, true); - } else { - setSelected(it, false); - } - } - } - } else { - for (T it : newObjects) { - for (T selected : selectedSet) { - if (selected.equals(it)) { - setSelected(it, true); - } else { - setSelected(it, false); - } - } + for (final T it : newObjects) { + for (final T selected : selectedSet) { + setSelected(it, comp == null ? selected.equals(it) : comp.representSameEntity(selected, it)); } } } From dcd2effc05ce5e6d7f566fa1ed5dd3716675a887 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Fri, 20 Nov 2015 16:27:49 +0100 Subject: [PATCH 022/306] revised the RefreshableSelectionModel --- .../AbstractLeaderboardConfigPanel.java | 2 +- .../AbstractTrackedRacesListComposite.java | 2 +- .../ui/adminconsole/EventListComposite.java | 14 +++---- .../LeaderboardGroupConfigPanel.java | 4 +- .../ui/adminconsole/RegattaListComposite.java | 2 +- .../StructureImportListComposite.java | 2 +- .../gwt/ui/adminconsole/TableWrapper.java | 2 +- .../TracTracEventManagementPanel.java | 2 +- .../controls/SelectionCheckboxColumn.java | 15 +++++--- .../gwt/ui/leaderboard/LeaderboardPanel.java | 2 +- .../RefreshableMultiSelectionModel.java | 37 ++++++------------- .../celltable/RefreshableSelectionModel.java | 26 +++++++------ .../RefreshableSingleSelectionModel.java | 18 ++++----- 13 files changed, 59 insertions(+), 69 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index ecf918e0008..1c24cbc4d20 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -267,7 +267,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement final CellTable leaderboardTable, AdminConsoleTableResources tableResources, ListHandler leaderboardColumnListHandler) { SelectionCheckboxColumn selectionCheckboxColumn = new SelectionCheckboxColumn(tableResources.cellTableStyle().cellTableCheckboxSelected(), - tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()) { + tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { @Override protected ListDataProvider getListDataProvider() { return leaderboardList; 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 ba07f527a4f..fe16fdeda07 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 @@ -104,7 +104,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl protected void createUI() { AdminConsoleTableResources tableResources = GWT.create(AdminConsoleTableResources.class); this.selectionCheckboxColumn = new SelectionCheckboxColumn(tableResources.cellTableStyle().cellTableCheckboxSelected(), - tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()) { + tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { @Override protected ListDataProvider getListDataProvider() { return raceList; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 24e4de1e52f..61e51ed6d6b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -147,12 +147,6 @@ public class EventListComposite extends Composite implements EventsRefresher, Le @SuppressWarnings("unchecked") RefreshableMultiSelectionModel multiSelectionModel = (RefreshableMultiSelectionModel) eventTable.getSelectionModel(); - multiSelectionModel.setHasEqualIdentity(new EntityIdentityComparator() { - @Override - public boolean representSameEntity(EventDTO dto1, EventDTO dto2) { - return dto1.id.equals(dto2.id) ? true : false; - } - }); eventSelectionModel = multiSelectionModel; eventSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { @@ -205,7 +199,13 @@ public class EventListComposite extends Composite implements EventsRefresher, Le table.setWidth("100%"); SelectionCheckboxColumn eventSelectionCheckboxColumn = new SelectionCheckboxColumn(tableRes.cellTableStyle().cellTableCheckboxSelected(), - tableRes.cellTableStyle().cellTableCheckboxDeselected(), tableRes.cellTableStyle().cellTableCheckboxColumnCell()) { + tableRes.cellTableStyle().cellTableCheckboxDeselected(), + tableRes.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { + @Override + public boolean representSameEntity(EventDTO dto1, EventDTO dto2) { + return dto1.id.equals(dto2.id) ? true : false; + } + } /*entityIdentityComparator to create a RefreshableSelectionModel*/) { @Override protected ListDataProvider getListDataProvider() { return eventListDataProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java index a0bc37b92ea..fe6b8c92c7d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java @@ -192,7 +192,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements leaderboardsTable.ensureDebugId("LeaderboardsCellTable"); SelectionCheckboxColumn leaderboardTableSelectionColumn = new SelectionCheckboxColumn( - tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()) { + tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { @Override protected ListDataProvider getListDataProvider() { return leaderboardsProvider; @@ -365,7 +365,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements groupDetailsProvider.addDataDisplay(groupDetailsTable); SelectionCheckboxColumn groupDetailsTableSelectionColumn = new SelectionCheckboxColumn( - tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()) { + tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { @Override protected ListDataProvider getListDataProvider() { return groupDetailsProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index d964d25e4e5..9dd1c32d230 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -147,7 +147,7 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer table.setWidth("100%"); SelectionCheckboxColumn regattaSelectionCheckboxColumn = new SelectionCheckboxColumn(tableRes.cellTableStyle().cellTableCheckboxSelected(), - tableRes.cellTableStyle().cellTableCheckboxDeselected(), tableRes.cellTableStyle().cellTableCheckboxColumnCell()) { + tableRes.cellTableStyle().cellTableCheckboxDeselected(), tableRes.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { @Override protected ListDataProvider getListDataProvider() { return regattaListDataProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java index 427e9e40044..686dbf75339 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java @@ -42,7 +42,7 @@ public class StructureImportListComposite extends RegattaListComposite implement this.selectionCheckboxColumn = new SelectionCheckboxColumn(tableRes.cellTableStyle() .cellTableCheckboxSelected(), tableRes.cellTableStyle().cellTableCheckboxDeselected(), - tableRes.cellTableStyle().cellTableCheckboxColumnCell()) { + tableRes.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { @Override protected ListDataProvider getListDataProvider() { return regattaListDataProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java index 90c2038daa8..09c7bb16d4b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java @@ -49,7 +49,7 @@ public abstract class TableWrapper> implements Is SelectionCheckboxColumn selectionCheckboxColumn = new SelectionCheckboxColumn( tableRes.cellTableStyle().cellTableCheckboxSelected(), tableRes.cellTableStyle().cellTableCheckboxDeselected(), - tableRes.cellTableStyle().cellTableCheckboxColumnCell()) { + tableRes.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { @Override protected ListDataProvider getListDataProvider() { return dataProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java index 040c531102c..4f31ef5305e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java @@ -359,7 +359,7 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { raceVisibilityColumn.setSortable(true); SelectionCheckboxColumn selectionCheckboxColumn = new SelectionCheckboxColumn(tableResources.cellTableStyle().cellTableCheckboxSelected(), - tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()) { + tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { @Override public Boolean getValue(TracTracRaceRecordDTO row) { return racesTable.getSelectionModel().isSelected(row); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java index d2c27f7a405..b47c8ed3289 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java @@ -19,6 +19,7 @@ import com.sap.sailing.domain.common.InvertibleComparator; import com.sap.sailing.domain.common.SortingOrder; import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; import com.sap.sailing.domain.common.impl.InvertibleComparatorAdapter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; /** @@ -56,18 +57,20 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW * CSS class for the <div> element representing a deselected element * @param checkboxColumnCellCSSClass * CSS class for the <td> element rendering the cell + * @param entityIdentityComparator + * {@link EntityIdentityComparator} to create a {@link RefreshableMultiSelectionModel}. */ protected SelectionCheckboxColumn(String selectedCheckboxCSSClass, - String deselectedCheckboxCSSClass, String checkboxColumnCellCSSClass) { - this(new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass); + String deselectedCheckboxCSSClass, String checkboxColumnCellCSSClass, EntityIdentityComparator entityIdentityComparator) { + this(new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass, entityIdentityComparator); } - private SelectionCheckboxColumn(BetterCheckboxCell checkboxCell, String checkboxColumnCellCSSClass) { + private SelectionCheckboxColumn(BetterCheckboxCell checkboxCell, String checkboxColumnCellCSSClass, EntityIdentityComparator entityIdentityComparator) { super(checkboxCell, SortingOrder.DESCENDING); this.cell = checkboxCell; this.checkboxColumnCellCSSClass = checkboxColumnCellCSSClass; this.selectionEventTranslator = createSelectionEventTranslator(); - this.selectionModel = createSelectionModel(); + this.selectionModel = createSelectionModel(entityIdentityComparator); } /** @@ -96,8 +99,8 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW * Otherwise, clients or subclasses are responsible to issue the necessary calls to {@link #redrawRow(LeaderboardRowDTO, List)} * after selection changes. */ - private RefreshableMultiSelectionModel createSelectionModel() { - return new RefreshableMultiSelectionModel() { + private RefreshableMultiSelectionModel createSelectionModel(final EntityIdentityComparator entityIdentityComparator) { + return new RefreshableMultiSelectionModel(entityIdentityComparator) { @Override public void clear() { super.clear(); 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 11c7854898e..49d5dcf0ab9 100755 --- 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 @@ -1593,7 +1593,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS private class LeaderboardSelectionCheckboxColumn extends com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn implements CompetitorSelectionChangeListener { protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) { - super(tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()); + super(tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/); competitorSelectionProvider.addCompetitorSelectionChangeListener(this); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 4886937d8d0..2aadb13c699 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -1,5 +1,6 @@ package com.sap.sse.gwt.client.celltable; +import java.util.HashSet; import java.util.Set; import com.google.gwt.view.client.MultiSelectionModel; @@ -28,36 +29,20 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im return comp; } - @Override - public void setHasEqualIdentity(EntityIdentityComparator comp) { - this.comp = comp; - } - @Override public void refreshSelectionModel(Iterable newObjects) { - Set selectedSet = getSelectedSet(); - EntityIdentityComparator comp = getEntityIdentityComparator(); + final Set selectedSet = new HashSet<>(getSelectedSet()); + final boolean isSelected = !selectedSet.isEmpty(); clear(); - if (comp != null) { - for (T it : newObjects) { - for (T selected : selectedSet) { - if (comp.representSameEntity(selected, it)) { - setSelected(it, true); - } else { - setSelected(it, false); - } - } - } - } else { - for (T it : newObjects) { - for (T selected : selectedSet) { - if (selected.equals(it)) { - setSelected(it, true); - } else { - setSelected(it, false); - } + for (final T it : newObjects) { + if (isSelected) { + for (final T selected : selectedSet) { + setSelected(it, comp == null ? selected.equals(it) : comp.representSameEntity(selected, it)); } + } else { + setSelected(it, false); } } + scheduleSelectionChangeEvent(); } -} +} \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java index a4c94f3deae..a77a1c7d653 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java @@ -2,6 +2,16 @@ package com.sap.sse.gwt.client.celltable; import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SetSelectionModel; +import com.google.gwt.view.client.SingleSelectionModel; + +/** + * The Interface {@link RefreshableSelectionModel} unify the refresh behavior of SelectionModels e.g. of a + * {@link SingleSelectionModel}. To refresh this SelectionModel you can call the + * {@link RefreshableSelectionModel#refreshSelectionModel(Iterable)} method and this method will refresh the + * SelectionModel. + * + * @author D064976 + */ public interface RefreshableSelectionModel extends SetSelectionModel { /** @@ -10,15 +20,6 @@ public interface RefreshableSelectionModel extends SetSelectionModel { */ public EntityIdentityComparator getEntityIdentityComparator(); - /** - * Sets the {@link EntityIdentityComparator} field, to compare objects from the type <T>. - * - * @param comp - * When you set null, the T.eqauls() will be used. - */ - // TODO remove this setter and pass the comparator to the constructor of the implementing class(es) - public void setHasEqualIdentity(EntityIdentityComparator comp); - /** * Refreshes the {@link RefreshableSelectionModel} with the newObjects. All objects from the current * selection that {@link EntityIdentityComparator#representSameEntity(Object, Object) represent the same entity} as @@ -26,9 +27,12 @@ public interface RefreshableSelectionModel extends SetSelectionModel { * has no {@link EntityIdentityComparator} set, this method will use the {@link #equals(Object)} method to compare. *

    * - * TODO what happens with objects that were in the selection but are not in newObjects? - * TODO what about triggering an {@link SelectionChangeEvent.Handler#onSelectionChange(SelectionChangeEvent)}, e.g., using {@link AbstractSelectionModel#scheduleSelectionChangeEvent} + * If an selected object is not in the newObjects the object wouldn't be selected anymore. + *

    * + * When the selection is refreshed this method triggers a + * {@link SelectionChangeEvent.Handler#onSelectionChange(SelectionChangeEvent) onSelectionChangedEvent} using + * {@link AbstractSelectionModel#scheduleSelectionChangeEvent}. */ public void refreshSelectionModel(Iterable newObjects); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index 5965683d480..b8e71daf722 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -1,5 +1,6 @@ package com.sap.sse.gwt.client.celltable; +import java.util.HashSet; import java.util.Set; import com.google.gwt.view.client.ProvidesKey; @@ -28,21 +29,18 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel return comp; } - @Override - public void setHasEqualIdentity(EntityIdentityComparator comp) { - this.comp = comp; - // TODO remove - } - @Override public void refreshSelectionModel(Iterable newObjects) { - final Set selectedSet = getSelectedSet(); - final EntityIdentityComparator comp = getEntityIdentityComparator(); + final Set selectedSet = new HashSet<>(getSelectedSet()); + final T selected = selectedSet.isEmpty() ? null : selectedSet.iterator().next(); clear(); for (final T it : newObjects) { - for (final T selected : selectedSet) { + if (selected != null) { setSelected(it, comp == null ? selected.equals(it) : comp.representSameEntity(selected, it)); + } else { + setSelected(it, false); } } + scheduleSelectionChangeEvent(); } -} +} \ No newline at end of file From a9a83680af1f0dff2643c6f15ec898dc10564271 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Fri, 20 Nov 2015 18:11:23 +0100 Subject: [PATCH 023/306] first steps to replace the RegattaSelectionModel --- .../ui/adminconsole/RegattaListComposite.java | 31 +++++++----- .../adminconsole/RegattaManagementPanel.java | 28 ++++++----- .../RegattaSelectionChangeListener.java | 11 +++-- .../gwt/ui/client/RegattaSelectionModel.java | 47 +++++++++++++------ .../ui/client/RegattaSelectionProvider.java | 11 +++-- 5 files changed, 81 insertions(+), 47 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index 9dd1c32d230..dc47c4c1f7a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -28,7 +28,6 @@ import com.google.gwt.view.client.SelectionChangeEvent; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.RegattaName; import com.sap.sailing.gwt.ui.client.RegattaRefresher; -import com.sap.sailing.gwt.ui.client.RegattaSelectionProvider; import com.sap.sailing.gwt.ui.client.RegattasDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -42,6 +41,8 @@ import com.sap.sse.common.Util; 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.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -51,7 +52,7 @@ import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; * */ public class RegattaListComposite extends Composite implements RegattasDisplayer { - +//TODO remove regattaSelectionModel protected final MultiSelectionModel regattaSelectionModel; protected final CellTable regattaTable; protected final ListDataProvider regattaListDataProvider; @@ -62,7 +63,7 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer private final Label noRegattasLabel; private final SailingServiceAsync sailingService; - protected final RegattaSelectionProvider regattaSelectionProvider; + protected final RefreshableMultiSelectionModel refreshableRegattaMultiSelectionModel; private final ErrorReporter errorReporter; private final RegattaRefresher regattaRefresher; protected final StringMessages stringMessages; @@ -79,10 +80,10 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer } public RegattaListComposite(final SailingServiceAsync sailingService, - final RegattaSelectionProvider regattaSelectionProvider, RegattaRefresher regattaRefresher, + final RefreshableMultiSelectionModel refreshableRegattaMultiSelectionModel, RegattaRefresher regattaRefresher, final ErrorReporter errorReporter, final StringMessages stringMessages) { this.sailingService = sailingService; - this.regattaSelectionProvider = regattaSelectionProvider; + this.refreshableRegattaMultiSelectionModel = refreshableRegattaMultiSelectionModel; this.regattaRefresher = regattaRefresher; this.errorReporter = errorReporter; this.stringMessages = stringMessages; @@ -125,12 +126,13 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer regattaSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { - List selectedRegattas = getSelectedRegattas(); +/* List selectedRegattas = getSelectedRegattas(); List selectedRaceIdentifiers = new ArrayList(); for (RegattaDTO selectedRegatta : selectedRegattas) { selectedRaceIdentifiers.add(selectedRegatta.getRegattaIdentifier()); } - RegattaListComposite.this.regattaSelectionProvider.setSelection(selectedRaceIdentifiers); + RegattaListComposite.this.regattaSelectionProvider.setSelection(selectedRaceIdentifiers);*/ + refreshableRegattaMultiSelectionModel.refreshSelectionModel(getSelectedRegattas()); } }); panel.add(regattaTable); @@ -147,7 +149,13 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer table.setWidth("100%"); SelectionCheckboxColumn regattaSelectionCheckboxColumn = new SelectionCheckboxColumn(tableRes.cellTableStyle().cellTableCheckboxSelected(), - tableRes.cellTableStyle().cellTableCheckboxDeselected(), tableRes.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { + tableRes.cellTableStyle().cellTableCheckboxDeselected(), + tableRes.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { + @Override + public boolean representSameEntity(RegattaDTO dto1, RegattaDTO dto2) { + return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); + } + } /* entityIdentityComparator to create a RefreshableSelectionModel */) { @Override protected ListDataProvider getListDataProvider() { return regattaListDataProvider; @@ -369,13 +377,14 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer } List newAllRegattas = new ArrayList(); Util.addAll(regattas, newAllRegattas); - List newAllRegattaIdentifiers = new ArrayList(); + /*List newAllRegattaIdentifiers = new ArrayList(); for (RegattaDTO regatta : regattas) { newAllRegattaIdentifiers.add(regatta.getRegattaIdentifier()); - } + }*/ allRegattas = newAllRegattas; filterablePanelRegattas.updateAll(allRegattas); - regattaSelectionProvider.setAllRegattas(newAllRegattaIdentifiers); + //regattaSelectionProvider.setAllRegattas(newAllRegattaIdentifiers); + refreshableRegattaMultiSelectionModel.refreshSelectionModel(regattas); } public List getAllRegattas() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java index 02497fabd54..c7030824391 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java @@ -19,9 +19,6 @@ import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; import com.sap.sailing.gwt.ui.client.RegattaRefresher; -import com.sap.sailing.gwt.ui.client.RegattaSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RegattaSelectionModel; -import com.sap.sailing.gwt.ui.client.RegattaSelectionProvider; import com.sap.sailing.gwt.ui.client.RegattasDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -30,6 +27,8 @@ import com.sap.sailing.gwt.ui.shared.RegattaDTO; import com.sap.sailing.gwt.ui.shared.SeriesDTO; import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; /** @@ -39,13 +38,13 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; * @author Frank Mittag (C5163974) * */ -public class RegattaManagementPanel extends SimplePanel implements RegattasDisplayer, RegattaSelectionChangeListener { +public class RegattaManagementPanel extends SimplePanel implements RegattasDisplayer/*, RegattaSelectionChangeListener*/ { private final SailingServiceAsync sailingService; private final ErrorReporter errorReporter; private final StringMessages stringMessages; private final RegattaRefresher regattaRefresher; - private RegattaSelectionProvider regattaSelectionProvider; + private final RefreshableMultiSelectionModel refreshableRegattaMultiSelectionModel; private Button removeRegattaButton; private RegattaListComposite regattaListComposite; @@ -92,8 +91,8 @@ public class RegattaManagementPanel extends SimplePanel implements RegattasDispl //Creating a new Collection, because getSelectedRegattas returns an //unmodifiable collection, which can't be sent to the server. Collection regattas = new HashSet(); - for (RegattaIdentifier regatta : regattaSelectionProvider.getSelectedRegattas()) { - regattas.add(regatta); + for (RegattaDTO regatta : refreshableRegattaMultiSelectionModel.getSelectedSet()) { + regattas.add(regatta.getRegattaIdentifier()); } removeRegattas(regattas); } @@ -102,10 +101,15 @@ public class RegattaManagementPanel extends SimplePanel implements RegattasDispl regattaManagementControlsPanel.add(removeRegattaButton); regattasContentPanel.add(regattaManagementControlsPanel); - regattaSelectionProvider = new RegattaSelectionModel(true); - regattaSelectionProvider.addRegattaSelectionChangeListener(this); + refreshableRegattaMultiSelectionModel = new RefreshableMultiSelectionModel<>(new EntityIdentityComparator() { + @Override + public boolean representSameEntity(RegattaDTO dto1, RegattaDTO dto2) { + return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); + } + }); + //refreshableRegattaMultiSelectionModel.addRegattaSelectionChangeListener(this); - regattaListComposite = new RegattaListComposite(sailingService, regattaSelectionProvider, regattaRefresher, errorReporter, stringMessages); + regattaListComposite = new RegattaListComposite(sailingService, refreshableRegattaMultiSelectionModel, regattaRefresher, errorReporter, stringMessages); regattaListComposite.ensureDebugId("RegattaListComposite"); regattasContentPanel.add(regattaListComposite); @@ -222,7 +226,7 @@ public class RegattaManagementPanel extends SimplePanel implements RegattasDispl regattaListComposite.fillRegattas(regattas); } - @Override +/* @Override public void onRegattaSelectionChange(List selectedRegattas) { final RegattaIdentifier selectedRegatta; if (selectedRegattas.size() == 1) { @@ -241,5 +245,5 @@ public class RegattaManagementPanel extends SimplePanel implements RegattasDispl regattaDetailsComposite.setVisible(false); } removeRegattaButton.setEnabled(!selectedRegattas.isEmpty()); - } + }*/ } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionChangeListener.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionChangeListener.java index df0cab75a6f..a5cf497597e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionChangeListener.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionChangeListener.java @@ -1,22 +1,23 @@ -package com.sap.sailing.gwt.ui.client; +/*package com.sap.sailing.gwt.ui.client; import java.util.List; import com.sap.sailing.domain.common.RegattaIdentifier; -/** +*//** * Allows UI components to observe a regatta selector, such as a drop-down box showing a list of regattas * * @author Axel Uhl (d043530) * - */ + *//* public interface RegattaSelectionChangeListener { - /** + *//** * The first element is the first one selected * * @param selectedRegattas a non-null list which is empty if nothing is selected - */ + *//* void onRegattaSelectionChange(List selectedRegattas); } +*/ \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java index 815785ae0c7..ed0c48029bd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java @@ -1,4 +1,4 @@ -package com.sap.sailing.gwt.ui.client; +/*package com.sap.sailing.gwt.ui.client; import java.util.ArrayList; import java.util.Arrays; @@ -8,6 +8,9 @@ import java.util.Iterator; import java.util.List; import java.util.Set; +import com.google.gwt.event.shared.GwtEvent; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; @@ -62,11 +65,11 @@ public class RegattaSelectionModel implements RegattaSelectionProvider, Refresha } } - /** + *//** * Sets the universe of all regattas from which this selection model may select. An equal list will subsequently be * returned by {@link #getAllRegattas()}. Regattas from the {@link #getSelectedRegattas() selection} not in * newAllRegattas are removed from the selection. If this happens, the selection listeners are notified. - */ + *//* @Override public void setAllRegattas(List newAllRegattas, RegattaSelectionChangeListener... listenersNotToNotify) { allRegattas.clear(); @@ -94,10 +97,10 @@ public class RegattaSelectionModel implements RegattaSelectionProvider, Refresha return hasMultiSelection; } - /** + *//** * Returns an instance of HasEqualIdentity<RegattaIdentifier>. The compare * method works the same way as o1.eqauls(o2). - */ + *//* @Override public EntityIdentityComparator getEntityIdentityComparator() { return new EntityIdentityComparator() { @@ -108,15 +111,6 @@ public class RegattaSelectionModel implements RegattaSelectionProvider, Refresha }; } - /** - * This method will have no impact on the object, because the only logical way to - * compare RegattaIdentifier objects is to use the equals method. - */ - @Override - public void setHasEqualIdentity(EntityIdentityComparator comp) { - //TODO remove - } - @Override public Set getSelectedSet() { Iterable selected = getSelectedRegattas(); @@ -158,4 +152,29 @@ public class RegattaSelectionModel implements RegattaSelectionProvider, Refresha setAllRegattas(all); } } + + @Override + public HandlerRegistration addSelectionChangeHandler(Handler handler) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isSelected(RegattaIdentifier object) { + // TODO Auto-generated method stub + return false; + } + + @Override + public void fireEvent(GwtEvent event) { + // TODO Auto-generated method stub + + } + + @Override + public Object getKey(RegattaIdentifier item) { + // TODO Auto-generated method stub + return null; + } } +*/ \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionProvider.java index b8f77a75335..4ac30d35b9b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionProvider.java @@ -1,13 +1,13 @@ -package com.sap.sailing.gwt.ui.client; +/*package com.sap.sailing.gwt.ui.client; import java.util.List; import com.sap.sailing.domain.common.RegattaIdentifier; public interface RegattaSelectionProvider { - /** + *//** * @return a non-null list which may be empty - */ + *//* List getSelectedRegattas(); void addRegattaSelectionChangeListener(RegattaSelectionChangeListener listener); @@ -18,10 +18,11 @@ public interface RegattaSelectionProvider { boolean hasMultiSelection(); - /** + *//** * @return a non-null list which may be empty - */ + *//* List getAllRegattas(); void setAllRegattas(List newAllRegattas, RegattaSelectionChangeListener... listenersNotToNotify); } +*/ \ No newline at end of file From 7223a239843ebc0ca237e4686f1508e355688c03 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sat, 21 Nov 2015 01:54:39 +0100 Subject: [PATCH 024/306] fixed typo (duh... KDiff3 on Linux is no fun) --- .../java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 1048493de96..4b04d8c7abe 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 @@ -233,7 +233,7 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi components.add(windChart); editMarkPassingPanel = new EditMarkPassingsPanel(sailingService, selectedRaceIdentifier, stringMessages, competitorSelectionProvider, errorReporter, timer); - editMarkPassingPanel.setLeaderboardN(aderboardPanel.getLeaderboard()); + editMarkPassingPanel.setLeaderboard(leaderboardPanel.getLeaderboard()); editMarkPassingPanel.getEntryWidget().setTitle(stringMessages.editMarkPassings()); components.add(editMarkPassingPanel); } @@ -361,7 +361,7 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi public void updatedLeaderboard(LeaderboardDTO leaderboard) { leaderboardAndMapViewer.setLeftComponentWidth(leaderboardPanel.getContentPanel().getOffsetWidth()); if (editMarkPassingPanel != null) { - editMarkPassingPanel.setLeaderboardNameaderboard); + editMarkPassingPanel.setLeaderboard(leaderboard); } } From 82067741a899bfbd42936300a2943f82c2092cf3 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 23 Nov 2015 10:55:53 +0100 Subject: [PATCH 025/306] replaced RegattaSelectionModel --- .../ui/adminconsole/RegattaListComposite.java | 37 ++++++++----------- .../StructureImportListComposite.java | 15 ++++---- .../StructureImportManagementPanel.java | 17 ++++++--- .../controls/SelectionCheckboxColumn.java | 20 ++++++++-- 4 files changed, 53 insertions(+), 36 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index dc47c4c1f7a..1c572ca2238 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -10,7 +10,6 @@ import java.util.List; import com.google.gwt.cell.client.AbstractCell; import com.google.gwt.cell.client.FieldUpdater; import com.google.gwt.core.client.GWT; -import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.user.cellview.client.CellTable; @@ -23,7 +22,6 @@ import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.MultiSelectionModel; import com.google.gwt.view.client.SelectionChangeEvent; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.RegattaName; @@ -41,7 +39,6 @@ import com.sap.sse.common.Util; 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.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -53,7 +50,7 @@ import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; */ public class RegattaListComposite extends Composite implements RegattasDisplayer { //TODO remove regattaSelectionModel - protected final MultiSelectionModel regattaSelectionModel; +// protected final MultiSelectionModel regattaSelectionModel; protected final CellTable regattaTable; protected final ListDataProvider regattaListDataProvider; private List allRegattas; @@ -100,7 +97,7 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer panel.add(noRegattasLabel); regattaListDataProvider = new ListDataProvider(); - regattaTable = createRegattaTable(); + regattaTable = createRegattaTable(this.refreshableRegattaMultiSelectionModel); regattaTable.ensureDebugId("RegattasCellTable"); regattaTable.setVisible(false); @@ -118,12 +115,12 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer }; filterablePanelRegattas.getTextBox().ensureDebugId("RegattasFilterTextBox"); panel.add(filterablePanelRegattas); - +/* @SuppressWarnings("unchecked") MultiSelectionModel multiSelectionModel = (MultiSelectionModel) regattaTable.getSelectionModel(); regattaSelectionModel = multiSelectionModel; - - regattaSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { +*/ + this.refreshableRegattaMultiSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { /* List selectedRegattas = getSelectedRegattas(); @@ -132,30 +129,25 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer selectedRaceIdentifiers.add(selectedRegatta.getRegattaIdentifier()); } RegattaListComposite.this.regattaSelectionProvider.setSelection(selectedRaceIdentifiers);*/ - refreshableRegattaMultiSelectionModel.refreshSelectionModel(getSelectedRegattas()); + refreshableRegattaMultiSelectionModel.refreshSelectionModel(allRegattas); } }); panel.add(regattaTable); initWidget(mainPanel); } - + /* public HandlerRegistration addSelectionChangeHandler(SelectionChangeEvent.Handler handler) { - return regattaSelectionModel.addSelectionChangeHandler(handler); + return refreshableRegattaMultiSelectionModel.addSelectionChangeHandler(handler); } - - protected CellTable createRegattaTable() { +*/ + protected CellTable createRegattaTable(RefreshableMultiSelectionModel selectionmodel) { CellTable table = new CellTable(/* pageSize */10000, tableRes); regattaListDataProvider.addDataDisplay(table); table.setWidth("100%"); - SelectionCheckboxColumn regattaSelectionCheckboxColumn = new SelectionCheckboxColumn(tableRes.cellTableStyle().cellTableCheckboxSelected(), + SelectionCheckboxColumn regattaSelectionCheckboxColumn = new SelectionCheckboxColumn(selectionmodel, tableRes.cellTableStyle().cellTableCheckboxSelected(), tableRes.cellTableStyle().cellTableCheckboxDeselected(), - tableRes.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { - @Override - public boolean representSameEntity(RegattaDTO dto1, RegattaDTO dto2) { - return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); - } - } /* entityIdentityComparator to create a RefreshableSelectionModel */) { + tableRes.cellTableStyle().cellTableCheckboxColumnCell()) { @Override protected ListDataProvider getListDataProvider() { return regattaListDataProvider; @@ -355,15 +347,18 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer } protected List getSelectedRegattas() { + /* List result = new ArrayList(); if (regattaListDataProvider != null) { for (RegattaDTO regatta : regattaListDataProvider.getList()) { - if (regattaSelectionModel.isSelected(regatta)) { + if (refreshableRegattaMultiSelectionModel.isSelected(regatta)) { result.add(regatta); } } } return result; + */ + return new ArrayList(refreshableRegattaMultiSelectionModel.getSelectedSet()); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java index 686dbf75339..5e1edc6c4e2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java @@ -7,7 +7,6 @@ import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; import com.google.gwt.user.cellview.client.TextColumn; import com.google.gwt.view.client.ListDataProvider; import com.sap.sailing.gwt.ui.client.RegattaRefresher; -import com.sap.sailing.gwt.ui.client.RegattaSelectionProvider; import com.sap.sailing.gwt.ui.client.RegattasDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -15,6 +14,7 @@ import com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn; import com.sap.sailing.gwt.ui.shared.RegattaDTO; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; public class StructureImportListComposite extends RegattaListComposite implements RegattasDisplayer { @@ -26,23 +26,24 @@ public class StructureImportListComposite extends RegattaListComposite implement } public StructureImportListComposite(final SailingServiceAsync sailingService, - final RegattaSelectionProvider regattaSelectionProvider, RegattaRefresher regattaRefresher, + final RefreshableMultiSelectionModel refreshableRegattaSelectionModel, RegattaRefresher regattaRefresher, RegattaStructureProvider regattaStructureProvider, final ErrorReporter errorReporter, final StringMessages stringMessages) { - super(sailingService, regattaSelectionProvider, regattaRefresher, errorReporter, stringMessages); + super(sailingService, refreshableRegattaSelectionModel, regattaRefresher, errorReporter, stringMessages); this.regattaStructureProvider = regattaStructureProvider; } // create Regatta Table in StructureImportManagementPanel @Override - protected CellTable createRegattaTable() { + protected CellTable createRegattaTable(RefreshableMultiSelectionModel refreshableRegattaSelectionModel) { CellTable table = new CellTable(/* pageSize */10000, tableRes); regattaListDataProvider.addDataDisplay(table); table.setWidth("100%"); - this.selectionCheckboxColumn = new SelectionCheckboxColumn(tableRes.cellTableStyle() - .cellTableCheckboxSelected(), tableRes.cellTableStyle().cellTableCheckboxDeselected(), - tableRes.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { + this.selectionCheckboxColumn = new SelectionCheckboxColumn(refreshableRegattaSelectionModel, + tableRes.cellTableStyle().cellTableCheckboxSelected(), + tableRes.cellTableStyle().cellTableCheckboxDeselected(), + tableRes.cellTableStyle().cellTableCheckboxColumnCell()) { @Override protected ListDataProvider getListDataProvider() { return regattaListDataProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java index 54aee742cbc..5c22edf8c3e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java @@ -29,14 +29,11 @@ import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.view.client.SelectionChangeEvent; -import com.google.gwt.view.client.SelectionChangeEvent.Handler; import com.sap.sailing.domain.common.RankingMetrics; import com.sap.sailing.domain.common.ScoringSchemeType; import com.sap.sailing.domain.common.dto.BoatClassDTO; import com.sap.sailing.gwt.ui.adminconsole.StructureImportListComposite.RegattaStructureProvider; import com.sap.sailing.gwt.ui.client.RegattaRefresher; -import com.sap.sailing.gwt.ui.client.RegattaSelectionModel; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.CourseAreaDTO; @@ -46,6 +43,8 @@ import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; import com.sap.sailing.gwt.ui.shared.RegattaDTO; import com.sap.sailing.gwt.ui.shared.SeriesDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; @@ -103,8 +102,14 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat this.errorReporter = errorReporter; this.stringMessages = stringMessages; this.regattaRefresher = regattaRefresher; - this.regattaListComposite = new StructureImportListComposite(this.sailingService, new RegattaSelectionModel( - true), this.regattaRefresher, this, this.errorReporter, this.stringMessages); + this.regattaListComposite = new StructureImportListComposite(this.sailingService, + new RefreshableMultiSelectionModel(new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(RegattaDTO dto1, RegattaDTO dto2) { + return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); + } + }), this.regattaRefresher, this, this.errorReporter, this.stringMessages); regattaListComposite.ensureDebugId("RegattaListComposite"); VerticalPanel mainPanel = new VerticalPanel(); @@ -112,6 +117,7 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat mainPanel.setWidth("100%"); createUI(mainPanel); + /* regattaListComposite.addSelectionChangeHandler(new Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { @@ -119,6 +125,7 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat updateImportRegattasButtonEnabledness(); } }); + */ regattaStructureGrid = new FlexTable(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java index b47c8ed3289..6d1e80a9ce1 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java @@ -60,11 +60,25 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW * @param entityIdentityComparator * {@link EntityIdentityComparator} to create a {@link RefreshableMultiSelectionModel}. */ - protected SelectionCheckboxColumn(String selectedCheckboxCSSClass, - String deselectedCheckboxCSSClass, String checkboxColumnCellCSSClass, EntityIdentityComparator entityIdentityComparator) { - this(new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass, entityIdentityComparator); + protected SelectionCheckboxColumn(RefreshableMultiSelectionModel selectionmodel, String selectedCheckboxCSSClass, + String deselectedCheckboxCSSClass, String checkboxColumnCellCSSClass) { + this(selectionmodel, new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass); + } + + protected SelectionCheckboxColumn(String selectedCheckboxCSSClass, String checkboxColumnCellCSSClass, + String deselectedCheckboxCSSClass, EntityIdentityComparator entityIdentityComparator) { + this(new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass, + entityIdentityComparator); } + private SelectionCheckboxColumn(RefreshableMultiSelectionModel selectionModel, BetterCheckboxCell checkboxCell, String checkboxColumnCellCSSClass) { + super(checkboxCell, SortingOrder.DESCENDING); + this.cell = checkboxCell; + this.checkboxColumnCellCSSClass = checkboxColumnCellCSSClass; + this.selectionEventTranslator = createSelectionEventTranslator(); + this.selectionModel = selectionModel; + } + private SelectionCheckboxColumn(BetterCheckboxCell checkboxCell, String checkboxColumnCellCSSClass, EntityIdentityComparator entityIdentityComparator) { super(checkboxCell, SortingOrder.DESCENDING); this.cell = checkboxCell; From 258348ba9f2dc2b71d650d438b5f2601665c7fd8 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 23 Nov 2015 11:42:06 +0100 Subject: [PATCH 026/306] newSelectionModel: modified EventSelectionHandler --- .../sailing/gwt/ui/adminconsole/RegattaListComposite.java | 6 +++--- .../gwt/ui/adminconsole/StructureImportManagementPanel.java | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index 1c572ca2238..e1ac6d1da32 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -10,6 +10,7 @@ import java.util.List; import com.google.gwt.cell.client.AbstractCell; import com.google.gwt.cell.client.FieldUpdater; import com.google.gwt.core.client.GWT; +import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeHtmlBuilder; import com.google.gwt.user.cellview.client.CellTable; @@ -49,7 +50,6 @@ import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; * */ public class RegattaListComposite extends Composite implements RegattasDisplayer { -//TODO remove regattaSelectionModel // protected final MultiSelectionModel regattaSelectionModel; protected final CellTable regattaTable; protected final ListDataProvider regattaListDataProvider; @@ -135,11 +135,11 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer panel.add(regattaTable); initWidget(mainPanel); } - /* + public HandlerRegistration addSelectionChangeHandler(SelectionChangeEvent.Handler handler) { return refreshableRegattaMultiSelectionModel.addSelectionChangeHandler(handler); } -*/ + protected CellTable createRegattaTable(RefreshableMultiSelectionModel selectionmodel) { CellTable table = new CellTable(/* pageSize */10000, tableRes); regattaListDataProvider.addDataDisplay(table); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java index 5c22edf8c3e..01a5a0c7c66 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java @@ -29,6 +29,8 @@ import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; import com.sap.sailing.domain.common.RankingMetrics; import com.sap.sailing.domain.common.ScoringSchemeType; import com.sap.sailing.domain.common.dto.BoatClassDTO; @@ -117,7 +119,7 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat mainPanel.setWidth("100%"); createUI(mainPanel); - /* + regattaListComposite.addSelectionChangeHandler(new Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { @@ -125,7 +127,7 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat updateImportRegattasButtonEnabledness(); } }); - */ + regattaStructureGrid = new FlexTable(); } From d01e8321acd51d4e90695e3cbf3fc2ad036f3419 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 23 Nov 2015 12:09:14 +0100 Subject: [PATCH 027/306] revised SelectionChangeHandler --- .../adminconsole/RegattaManagementPanel.java | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java index c7030824391..79d90d5b05b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java @@ -1,5 +1,6 @@ package com.sap.sailing.gwt.ui.adminconsole; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -15,6 +16,8 @@ import com.google.gwt.user.client.ui.CaptionPanel; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; @@ -107,6 +110,30 @@ public class RegattaManagementPanel extends SimplePanel implements RegattasDispl return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); } }); + refreshableRegattaMultiSelectionModel.addSelectionChangeHandler(new Handler() { + + @Override + public void onSelectionChange(SelectionChangeEvent event) { + List selectedRegattas = new ArrayList<>(refreshableRegattaMultiSelectionModel.getSelectedSet()); + final RegattaIdentifier selectedRegatta; + if (selectedRegattas.size() == 1) { + selectedRegatta = selectedRegattas.iterator().next().getRegattaIdentifier(); + if (selectedRegatta != null && regattaListComposite.getAllRegattas() != null) { + for (RegattaDTO regattaDTO : regattaListComposite.getAllRegattas()) { + if (regattaDTO.getRegattaIdentifier().equals(selectedRegatta)) { + regattaDetailsComposite.setRegatta(regattaDTO); + regattaDetailsComposite.setVisible(true); + break; + } + } + } + } else { + regattaDetailsComposite.setRegatta(null); + regattaDetailsComposite.setVisible(false); + } + removeRegattaButton.setEnabled(!selectedRegattas.isEmpty()); + } + }); //refreshableRegattaMultiSelectionModel.addRegattaSelectionChangeListener(this); regattaListComposite = new RegattaListComposite(sailingService, refreshableRegattaMultiSelectionModel, regattaRefresher, errorReporter, stringMessages); @@ -225,25 +252,4 @@ public class RegattaManagementPanel extends SimplePanel implements RegattasDispl public void fillRegattas(Iterable regattas) { regattaListComposite.fillRegattas(regattas); } - -/* @Override - public void onRegattaSelectionChange(List selectedRegattas) { - final RegattaIdentifier selectedRegatta; - if (selectedRegattas.size() == 1) { - selectedRegatta = selectedRegattas.iterator().next(); - if (selectedRegatta != null && regattaListComposite.getAllRegattas() != null) { - for (RegattaDTO regattaDTO : regattaListComposite.getAllRegattas()) { - if (regattaDTO.getRegattaIdentifier().equals(selectedRegatta)) { - regattaDetailsComposite.setRegatta(regattaDTO); - regattaDetailsComposite.setVisible(true); - break; - } - } - } - } else { - regattaDetailsComposite.setRegatta(null); - regattaDetailsComposite.setVisible(false); - } - removeRegattaButton.setEnabled(!selectedRegattas.isEmpty()); - }*/ } From f76c8c2e1e2abd02f94004141730817259f3e4c2 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Mon, 23 Nov 2015 14:44:38 +0100 Subject: [PATCH 028/306] Added ResizableFlowPanel to be able to fix hierarchical resizing --- .../gwt/ui/raceboard/ResizableFlowPanel.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/ResizableFlowPanel.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/ResizableFlowPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/ResizableFlowPanel.java new file mode 100644 index 00000000000..d83d0a64407 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/ResizableFlowPanel.java @@ -0,0 +1,23 @@ +package com.sap.sailing.gwt.ui.raceboard; + +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.user.client.ui.WidgetCollection; + +/** + * Utility class extending {@link FlowPanel} and implementing {@link RequiresResize} interface. All + * {@link #getChildren() children} also implementing {@link RequiresResize} are informed by calling their + * {@link RequiresResize#onResize()} method. + */ +class ResizableFlowPanel extends FlowPanel implements RequiresResize { + @Override + public void onResize() { + WidgetCollection children = getChildren(); + for (Widget widget : children) { + if (widget instanceof RequiresResize) { + ((RequiresResize) widget).onResize(); + } + } + } +} From 0c0e490ae6b16bf4a37fce8ebd35b032dd73804c Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Mon, 23 Nov 2015 14:47:15 +0100 Subject: [PATCH 029/306] Adjusted leaderboards splitpanel sizing to make sure it is not too wide on small screens/mobile devices --- .../gwt/ui/raceboard/RaceBoardPanel.java | 893 +++++++++--------- .../raceboard/SideBySideComponentViewer.java | 612 ++++++------ .../ui/raceboard/TouchSplitLayoutPanel.java | 18 + 3 files changed, 780 insertions(+), 743 deletions(-) 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 4b04d8c7abe..b1820943443 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 @@ -1,441 +1,452 @@ -package com.sap.sailing.gwt.ui.raceboard; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import com.google.gwt.core.shared.GWT; -import com.google.gwt.dom.client.Document; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.i18n.client.DateTimeFormat; -import com.google.gwt.user.client.Command; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.Anchor; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.MenuBar; -import com.google.gwt.user.client.ui.SimplePanel; -import com.google.gwt.user.client.ui.UIObject; -import com.sap.sailing.domain.common.LeaderboardNameConstants; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.dto.BoatDTO; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.domain.common.dto.RaceDTO; -import com.sap.sailing.gwt.ui.client.CompetitorColorProvider; -import com.sap.sailing.gwt.ui.client.CompetitorColorProviderImpl; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionModel; -import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; -import com.sap.sailing.gwt.ui.client.GlobalNavigationPanel; -import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; -import com.sap.sailing.gwt.ui.client.MediaServiceAsync; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; -import com.sap.sailing.gwt.ui.client.RaceTimePanel; -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.sailing.gwt.ui.client.media.MediaPlayerManagerComponent; -import com.sap.sailing.gwt.ui.client.media.PopupPositionProvider; -import com.sap.sailing.gwt.ui.client.shared.charts.EditMarkPassingsPanel; -import com.sap.sailing.gwt.ui.client.shared.charts.MultiCompetitorRaceChart; -import com.sap.sailing.gwt.ui.client.shared.charts.WindChart; -import com.sap.sailing.gwt.ui.client.shared.charts.WindChartSettings; -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.client.shared.racemap.RaceMap; -import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapResources; -import com.sap.sailing.gwt.ui.leaderboard.CompetitorFilterPanel; -import com.sap.sailing.gwt.ui.leaderboard.ExplicitRaceColumnSelectionWithPreselectedRace; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettingsFactory; -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.URLEncoder; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.TimeRangeWithZoomModel; -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.ComponentViewer; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.useragent.UserAgentDetails; -import com.sap.sse.security.ui.client.UserService; - -/** - * A view showing a list of components visualizing a race from the regattas announced by calls to {@link #fillRegattas(List)}. - * The race selection is provided by a {@link RaceSelectionProvider} for which this is a {@link RaceSelectionChangeListener listener}. - * {@link RaceIdentifier}-based race selection changes are converted to {@link RaceDTO} objects using the {@link #racesByIdentifier} - * map maintained during {@link #fillRegattas(List)}. The race selection provider is expected to be single selection only. - * - * @author Frank Mittag, Axel Uhl (d043530) - * - */ -public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeListener, LeaderboardUpdateListener, PopupPositionProvider { - private final SailingServiceAsync sailingService; - private final MediaServiceAsync mediaService; - private final UUID eventId; - private final StringMessages stringMessages; - private final ErrorReporter errorReporter; - private final RaceBoardViewConfiguration raceboardViewConfiguration; - private String raceBoardName; - - private final List componentViewers; - private RaceTimePanel timePanel; - private final Timer timer; - private final RaceSelectionProvider raceSelectionProvider; - private final UserAgentDetails userAgent; - private final CompetitorSelectionProvider competitorSelectionProvider; - private final TimeRangeWithZoomModel timeRangeWithZoomModel; - private final RegattaAndRaceIdentifier selectedRaceIdentifier; - - private final String leaderboardName; - private final LeaderboardPanel leaderboardPanel; - private WindChart windChart; - private MultiCompetitorRaceChart competitorChart; - private EditMarkPassingsPanel editMarkPassingPanel; - - /** - * The component viewer in ONESCREEN view mode. null if in CASCADE view mode - */ - private SideBySideComponentViewer leaderboardAndMapViewer; - - private final AsyncActionsExecutor asyncActionsExecutor; - - private final RaceTimesInfoProvider raceTimesInfoProvider; - private RaceMap raceMap; - - private final FlowPanel raceInformationHeader; - private final FlowPanel regattaAndRaceTimeInformationHeader; - private boolean currentRaceHasBeenSelectedOnce; - - private static final RaceMapResources raceMapResources = GWT.create(RaceMapResources.class); - - /** - * @param eventId - * an optional event that can be used for "back"-navigation in case the race board shows a race in the - * context of an event; may be null. - * @param isScreenLargeEnoughToOfferChartSupport - * if the screen is large enough to display charts such as the competitor chart or the wind chart, a - * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and - * the charts are created. - */ - public RaceBoardPanel(SailingServiceAsync sailingService, MediaServiceAsync mediaService, - UserService userService, AsyncActionsExecutor asyncActionsExecutor, Map competitorsAndTheirBoats, - Timer timer, RaceSelectionProvider theRaceSelectionProvider, String leaderboardName, - String leaderboardGroupName, UUID eventId, RaceBoardViewConfiguration raceboardViewConfiguration, - ErrorReporter errorReporter, final StringMessages stringMessages, - UserAgentDetails userAgent, RaceTimesInfoProvider raceTimesInfoProvider, boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport) { - this.sailingService = sailingService; - this.mediaService = mediaService; - this.stringMessages = stringMessages; - this.raceboardViewConfiguration = raceboardViewConfiguration; - this.raceSelectionProvider = theRaceSelectionProvider; - this.raceTimesInfoProvider = raceTimesInfoProvider; - this.errorReporter = errorReporter; - this.userAgent = userAgent; - this.timer = timer; - this.eventId = eventId; - this.currentRaceHasBeenSelectedOnce = false; - this.leaderboardName = leaderboardName; - raceSelectionProvider.addRaceSelectionChangeListener(this); - selectedRaceIdentifier = raceSelectionProvider.getSelectedRaces().iterator().next(); - this.setRaceBoardName(selectedRaceIdentifier.getRaceName()); - this.asyncActionsExecutor = asyncActionsExecutor; - FlowPanel mainPanel = new FlowPanel(); - mainPanel.setSize("100%", "100%"); - setWidget(mainPanel); - raceInformationHeader = new FlowPanel(); - raceInformationHeader.setStyleName("RegattaRaceInformation-Header"); - regattaAndRaceTimeInformationHeader = new FlowPanel(); - regattaAndRaceTimeInformationHeader.setStyleName("RegattaAndRaceTime-Header"); - timeRangeWithZoomModel = new TimeRangeWithZoomModel(); - componentViewers = new ArrayList(); - final CompetitorColorProvider colorProvider = new CompetitorColorProviderImpl(selectedRaceIdentifier, competitorsAndTheirBoats); - competitorSelectionProvider = new CompetitorSelectionModel(/* hasMultiSelection */ true, colorProvider); - - raceMapResources.combinedWindPanelStyle().ensureInjected(); - raceMap = new RaceMap(sailingService, asyncActionsExecutor, errorReporter, timer, - competitorSelectionProvider, stringMessages, showMapControls, getConfiguration().isShowViewStreamlets(), getConfiguration().isShowViewStreamletColors(), getConfiguration().isShowViewSimulation(), - selectedRaceIdentifier, raceMapResources.combinedWindPanelStyle(), /* showHeaderPanel */ true); - CompetitorFilterPanel competitorSearchTextBox = new CompetitorFilterPanel(competitorSelectionProvider, stringMessages, raceMap, - new LeaderboardFetcher() { - @Override - public LeaderboardDTO getLeaderboard() { - return leaderboardPanel.getLeaderboard(); - } - }, selectedRaceIdentifier); - raceMap.getLeftHeaderPanel().add(raceInformationHeader); - raceMap.getRightHeaderPanel().add(regattaAndRaceTimeInformationHeader); - - boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard = Document.get().getClientWidth() >= 1024; - leaderboardPanel = createLeaderboardPanel(leaderboardName, leaderboardGroupName, competitorSearchTextBox, isScreenLargeEnoughToInitiallyDisplayLeaderboard); - leaderboardPanel.setTitle(stringMessages.leaderboard()); - leaderboardPanel.getElement().getStyle().setMarginLeft(6, Unit.PX); - leaderboardPanel.getElement().getStyle().setMarginTop(10, Unit.PX); - createOneScreenView(leaderboardName, leaderboardGroupName, eventId, mainPanel, showMapControls, isScreenLargeEnoughToOfferChartSupport, isScreenLargeEnoughToInitiallyDisplayLeaderboard, raceMap, userService); // initializes the raceMap field - leaderboardPanel.addLeaderboardUpdateListener(this); - // in case the URL configuration contains the name of a competitors filter set we try to activate it - // FIXME the competitorsFilterSets has now moved to CompetitorSearchTextBox (which should probably be renamed); pass on the parameters to the LeaderboardPanel and see what it does with it - if (raceboardViewConfiguration.getActiveCompetitorsFilterSetName() != null) { - for (FilterSet> filterSet : competitorSearchTextBox.getCompetitorsFilterSets() - .getFilterSets()) { - if (filterSet.getName().equals(raceboardViewConfiguration.getActiveCompetitorsFilterSetName())) { - competitorSearchTextBox.getCompetitorsFilterSets().setActiveFilterSet(filterSet); - break; - } - } - } - timePanel = new RaceTimePanel(timer, timeRangeWithZoomModel, stringMessages, raceTimesInfoProvider, - raceboardViewConfiguration.isCanReplayDuringLiveRaces(), isScreenLargeEnoughToOfferChartSupport); - timeRangeWithZoomModel.addTimeZoomChangeListener(timePanel); - raceTimesInfoProvider.addRaceTimesInfoProviderListener(timePanel); - raceSelectionProvider.addRaceSelectionChangeListener(timePanel); - timePanel.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); - } - - /** - * @param event - * an optional event; may be null or else can be used to show some context information in - * the {@link GlobalNavigationPanel}. - * @param isScreenLargeEnoughToOfferChartSupport - * if the screen is large enough to display charts such as the competitor chart or the wind chart, a - * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and - * the charts are created. - * @param isScreenLargeEnoughToInitiallyDisplayLeaderboard TODO - */ - private void createOneScreenView(String leaderboardName, String leaderboardGroupName, UUID event, FlowPanel mainPanel, - boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport, boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard, RaceMap raceMap, UserService userService) { - // create the default leaderboard and select the right race - raceTimesInfoProvider.addRaceTimesInfoProviderListener(raceMap); - raceMap.onRaceSelectionChange(Collections.singletonList(selectedRaceIdentifier)); - List> components = new ArrayList>(); - if (isScreenLargeEnoughToOfferChartSupport) { - competitorChart = new MultiCompetitorRaceChart(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, - timer, timeRangeWithZoomModel, stringMessages, errorReporter, true, true, leaderboardGroupName, leaderboardName); - competitorChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); - competitorChart.getEntryWidget().setTitle(stringMessages.competitorCharts()); - competitorChart.setVisible(false); - components.add(competitorChart); - windChart = new WindChart(sailingService, raceSelectionProvider, timer, timeRangeWithZoomModel, new WindChartSettings(), - stringMessages, asyncActionsExecutor, errorReporter, /* compactChart */ true); - windChart.setVisible(false); - windChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); - windChart.getEntryWidget().setTitle(stringMessages.windChart()); - components.add(windChart); - editMarkPassingPanel = new EditMarkPassingsPanel(sailingService, selectedRaceIdentifier, - stringMessages, competitorSelectionProvider, errorReporter, timer); - editMarkPassingPanel.setLeaderboard(leaderboardPanel.getLeaderboard()); - editMarkPassingPanel.getEntryWidget().setTitle(stringMessages.editMarkPassings()); - components.add(editMarkPassingPanel); - } - boolean autoSelectMedia = getConfiguration().isAutoSelectMedia(); - MediaPlayerManagerComponent mediaPlayerManagerComponent = new MediaPlayerManagerComponent( - selectedRaceIdentifier, raceTimesInfoProvider, timer, mediaService, userService, stringMessages, - errorReporter, userAgent, this, autoSelectMedia); - leaderboardAndMapViewer = new SideBySideComponentViewer(leaderboardPanel, raceMap, mediaPlayerManagerComponent, components, stringMessages, userService, editMarkPassingPanel); - componentViewers.add(leaderboardAndMapViewer); - for (ComponentViewer componentViewer : componentViewers) { - mainPanel.add(componentViewer.getViewerWidget()); - } - boolean showLeaderboard = getConfiguration().isShowLeaderboard() && isScreenLargeEnoughToInitiallyDisplayLeaderboard; - setLeaderboardVisible(showLeaderboard); - if (isScreenLargeEnoughToOfferChartSupport) { - setWindChartVisible(getConfiguration().isShowWindChart()); - setCompetitorChartVisible(getConfiguration().isShowCompetitorsChart()); - } - // make sure to load leaderboard data for filtering to work - if (!showLeaderboard) { - leaderboardPanel.setVisible(true); - leaderboardPanel.setVisible(false); - } - } - - @SuppressWarnings("unused") - private void addSettingsMenuItem(MenuBar settingsMenu, final Component component) { - if (component.hasSettings()) { - settingsMenu.addItem(component.getLocalizedShortName(), new Command() { - public void execute() { - new SettingsDialog(component, stringMessages).show(); - } - }); - } - } - - private LeaderboardPanel createLeaderboardPanel(String leaderboardName, String leaderboardGroupName, - CompetitorFilterPanel competitorSearchTextBox, boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard) { - LeaderboardSettings leaderBoardSettings = LeaderboardSettingsFactory.getInstance() - .createNewSettingsForPlayMode(timer.getPlayMode(), - /* nameOfRaceToSort */ selectedRaceIdentifier.getRaceName(), - /* nameOfRaceColumnToShow */ null, /* nameOfRaceToShow */ selectedRaceIdentifier.getRaceName(), - new ExplicitRaceColumnSelectionWithPreselectedRace(selectedRaceIdentifier), /* showRegattaRank */ false, - /*showCompetitorSailIdColumn*/true, - /* don't showCompetitorFullNameColumn in case screen is so small that we don't - * even display the leaderboard initially */ isScreenLargeEnoughToInitiallyDisplayLeaderboard); - return new LeaderboardPanel(sailingService, asyncActionsExecutor, leaderBoardSettings, selectedRaceIdentifier != null, selectedRaceIdentifier, - competitorSelectionProvider, timer, leaderboardGroupName, leaderboardName, errorReporter, stringMessages, - userAgent, /* showRaceDetails */ true, competitorSearchTextBox, - /* showSelectionCheckbox */ true, raceTimesInfoProvider, /* autoExpandLastRaceColumn */ false, - /* don't adjust the timer's delay from the leaderboard; control it solely from the RaceTimesInfoProvider */ false, - /*autoApplyTopNFilter*/ false, /*showCompetitorFilterStatus*/ false); - } - - private void setComponentVisible(ComponentViewer componentViewer, Component component, boolean visible) { - component.setVisible(visible); - componentViewer.forceLayout(); - } - - /** - * Sets the collapsable panel for the leaderboard open or close, if in CASCADE view mode.
    - * Displays or hides the leaderboard, if in ONESCREEN view mode.

    - * - * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. - * - * @param visible true if the leaderboard shall be open/visible - */ - public void setLeaderboardVisible(boolean visible) { - setComponentVisible(leaderboardAndMapViewer, leaderboardPanel, visible); - } - - /** - * Sets the collapsable panel for the wind chart open or close, if in CASCADE view mode.
    - * Displays or hides the wind chart, if in ONESCREEN view mode.

    - * - * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. - * - * @param visible true if the wind chart shall be open/visible - */ - public void setWindChartVisible(boolean visible) { - setComponentVisible(leaderboardAndMapViewer, windChart, visible); - } - - /** - * Sets the collapsable panel for the competitor chart open or close, if in CASCADE view mode.
    - * Displays or hides the competitor chart, if in ONESCREEN view mode.

    - * - * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. - * - * @param visible true if the competitor chart shall be open/visible - */ - public void setCompetitorChartVisible(boolean visible) { - setComponentVisible(leaderboardAndMapViewer, competitorChart, visible); - } - - public RaceTimePanel getTimePanel() { - return timePanel; - } - - protected SailingServiceAsync getSailingService() { - return sailingService; - } - - protected String getRaceBoardName() { - return raceBoardName; - } - - protected void setRaceBoardName(String raceBoardName) { - this.raceBoardName = raceBoardName; - } - - protected ErrorReporter getErrorReporter() { - return errorReporter; - } - - @Override - public void onRaceSelectionChange(List selectedRaces) { - } - - public RaceBoardViewConfiguration getConfiguration() { - return raceboardViewConfiguration; - } - - @Override - public void updatedLeaderboard(LeaderboardDTO leaderboard) { - leaderboardAndMapViewer.setLeftComponentWidth(leaderboardPanel.getContentPanel().getOffsetWidth()); - if (editMarkPassingPanel != null) { - editMarkPassingPanel.setLeaderboard(leaderboard); - } - } - - @Override - public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { - if (!currentRaceHasBeenSelectedOnce) { - FleetDTO fleet = raceColumn.getFleet(raceIdentifier); - String seriesName = raceColumn.getSeriesName(); - if (LeaderboardNameConstants.DEFAULT_SERIES_NAME.equals(seriesName)) { - seriesName = ""; - } - String fleetForRaceName = fleet==null?"":fleet.getName(); - if (fleetForRaceName.equals(LeaderboardNameConstants.DEFAULT_FLEET_NAME)) { - fleetForRaceName = ""; - } else { - fleetForRaceName = " - "+fleetForRaceName; - } - final Label raceNameLabel = new Label(stringMessages.race() + " " + raceColumn.getRaceColumnName()); - raceNameLabel.setStyleName("RaceName-Label"); - final Label raceAdditionalInformationLabel = new Label(seriesName + fleetForRaceName); - raceAdditionalInformationLabel.setStyleName("RaceSeriesAndFleet-Label"); - raceInformationHeader.clear(); - raceInformationHeader.add(raceNameLabel); - raceInformationHeader.add(raceAdditionalInformationLabel); - final Anchor regattaNameAnchor = new Anchor(raceIdentifier.getRegattaName()); - if (eventId != null) { - // we don't use the EntryPointLinkFactory here, because of lacking support for Places - String debugParam = Window.Location.getParameter("gwt.codesvr"); - String link = "/gwt/Home.html"; - if (debugParam != null && !debugParam.isEmpty()) { - link += "?gwt.codesvr=" + debugParam; - } - link += "#EventPlace:eventId="+eventId.toString(); - link += "&navigationTab=Regatta&leaderboardName=" + URLEncoder.encode(leaderboardName); - regattaNameAnchor.setHref(link); - } else { - String leaderboardGroupNameParam = Window.Location.getParameter("leaderboardGroupName"); - if(leaderboardGroupNameParam != null) { - Map leaderboardGroupLinkParameters = new HashMap(); - leaderboardGroupLinkParameters.put("showRaceDetails", "true"); - leaderboardGroupLinkParameters.put("leaderboardGroupName", leaderboardGroupNameParam); - String leaderBoardGroupLink = EntryPointLinkFactory.createLeaderboardGroupLink(leaderboardGroupLinkParameters); - regattaNameAnchor.setHref(leaderBoardGroupLink); - } else { - // fallback - regattaNameAnchor.setHref("javascript:window.history.back();"); - } - } - regattaNameAnchor.setStyleName("RegattaName-Anchor"); - Label raceTimeLabel = computeRaceInformation(raceColumn, fleet); - raceTimeLabel.setStyleName("RaceTime-Label"); - regattaAndRaceTimeInformationHeader.clear(); - regattaAndRaceTimeInformationHeader.add(regattaNameAnchor); - regattaAndRaceTimeInformationHeader.add(raceTimeLabel); - currentRaceHasBeenSelectedOnce = true; - } - } - - @Override - public UIObject getXPositionUiObject() { - return timePanel; - } - - @Override - public UIObject getYPositionUiObject() { - return timePanel; - } - - private Label computeRaceInformation(RaceColumnDTO raceColumn, FleetDTO fleet) { - Label raceInformationLabel = new Label(); - raceInformationLabel.setStyleName("Race-Time-Label"); - DateTimeFormat formatter = DateTimeFormat.getFormat("E d/M/y"); - raceInformationLabel.setText(formatter.format(raceColumn.getStartDate(fleet))); - return raceInformationLabel; - } -} - +package com.sap.sailing.gwt.ui.raceboard; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.google.gwt.core.shared.GWT; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.i18n.client.DateTimeFormat; +import com.google.gwt.user.client.Command; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.Anchor; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.MenuBar; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.UIObject; +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.domain.common.LeaderboardNameConstants; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.dto.BoatDTO; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.gwt.ui.client.CompetitorColorProvider; +import com.sap.sailing.gwt.ui.client.CompetitorColorProviderImpl; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionModel; +import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; +import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; +import com.sap.sailing.gwt.ui.client.GlobalNavigationPanel; +import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; +import com.sap.sailing.gwt.ui.client.MediaServiceAsync; +import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; +import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; +import com.sap.sailing.gwt.ui.client.RaceTimePanel; +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.sailing.gwt.ui.client.media.MediaPlayerManagerComponent; +import com.sap.sailing.gwt.ui.client.media.PopupPositionProvider; +import com.sap.sailing.gwt.ui.client.shared.charts.EditMarkPassingsPanel; +import com.sap.sailing.gwt.ui.client.shared.charts.MultiCompetitorRaceChart; +import com.sap.sailing.gwt.ui.client.shared.charts.WindChart; +import com.sap.sailing.gwt.ui.client.shared.charts.WindChartSettings; +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.client.shared.racemap.RaceMap; +import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapResources; +import com.sap.sailing.gwt.ui.leaderboard.CompetitorFilterPanel; +import com.sap.sailing.gwt.ui.leaderboard.ExplicitRaceColumnSelectionWithPreselectedRace; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettingsFactory; +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.URLEncoder; +import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.player.TimeRangeWithZoomModel; +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.ComponentViewer; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.useragent.UserAgentDetails; +import com.sap.sse.security.ui.client.UserService; + +/** + * A view showing a list of components visualizing a race from the regattas announced by calls to {@link #fillRegattas(List)}. + * The race selection is provided by a {@link RaceSelectionProvider} for which this is a {@link RaceSelectionChangeListener listener}. + * {@link RaceIdentifier}-based race selection changes are converted to {@link RaceDTO} objects using the {@link #racesByIdentifier} + * map maintained during {@link #fillRegattas(List)}. The race selection provider is expected to be single selection only. + * + * @author Frank Mittag, Axel Uhl (d043530) + * + */ +public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeListener, + LeaderboardUpdateListener, PopupPositionProvider, RequiresResize { + private final SailingServiceAsync sailingService; + private final MediaServiceAsync mediaService; + private final UUID eventId; + private final StringMessages stringMessages; + private final ErrorReporter errorReporter; + private final RaceBoardViewConfiguration raceboardViewConfiguration; + private String raceBoardName; + + private final List componentViewers; + private RaceTimePanel timePanel; + private final Timer timer; + private final RaceSelectionProvider raceSelectionProvider; + private final UserAgentDetails userAgent; + private final CompetitorSelectionProvider competitorSelectionProvider; + private final TimeRangeWithZoomModel timeRangeWithZoomModel; + private final RegattaAndRaceIdentifier selectedRaceIdentifier; + + private final String leaderboardName; + private final LeaderboardPanel leaderboardPanel; + private WindChart windChart; + private MultiCompetitorRaceChart competitorChart; + private EditMarkPassingsPanel editMarkPassingPanel; + + /** + * The component viewer in ONESCREEN view mode. null if in CASCADE view mode + */ + private SideBySideComponentViewer leaderboardAndMapViewer; + + private final AsyncActionsExecutor asyncActionsExecutor; + + private final RaceTimesInfoProvider raceTimesInfoProvider; + private RaceMap raceMap; + + private final FlowPanel raceInformationHeader; + private final FlowPanel regattaAndRaceTimeInformationHeader; + private boolean currentRaceHasBeenSelectedOnce; + + private static final RaceMapResources raceMapResources = GWT.create(RaceMapResources.class); + + /** + * @param eventId + * an optional event that can be used for "back"-navigation in case the race board shows a race in the + * context of an event; may be null. + * @param isScreenLargeEnoughToOfferChartSupport + * if the screen is large enough to display charts such as the competitor chart or the wind chart, a + * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and + * the charts are created. + */ + public RaceBoardPanel(SailingServiceAsync sailingService, MediaServiceAsync mediaService, + UserService userService, AsyncActionsExecutor asyncActionsExecutor, Map competitorsAndTheirBoats, + Timer timer, RaceSelectionProvider theRaceSelectionProvider, String leaderboardName, + String leaderboardGroupName, UUID eventId, RaceBoardViewConfiguration raceboardViewConfiguration, + ErrorReporter errorReporter, final StringMessages stringMessages, + UserAgentDetails userAgent, RaceTimesInfoProvider raceTimesInfoProvider, boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport) { + this.sailingService = sailingService; + this.mediaService = mediaService; + this.stringMessages = stringMessages; + this.raceboardViewConfiguration = raceboardViewConfiguration; + this.raceSelectionProvider = theRaceSelectionProvider; + this.raceTimesInfoProvider = raceTimesInfoProvider; + this.errorReporter = errorReporter; + this.userAgent = userAgent; + this.timer = timer; + this.eventId = eventId; + this.currentRaceHasBeenSelectedOnce = false; + this.leaderboardName = leaderboardName; + raceSelectionProvider.addRaceSelectionChangeListener(this); + selectedRaceIdentifier = raceSelectionProvider.getSelectedRaces().iterator().next(); + this.setRaceBoardName(selectedRaceIdentifier.getRaceName()); + this.asyncActionsExecutor = asyncActionsExecutor; + FlowPanel mainPanel = new ResizableFlowPanel(); + mainPanel.setSize("100%", "100%"); + setWidget(mainPanel); + raceInformationHeader = new FlowPanel(); + raceInformationHeader.setStyleName("RegattaRaceInformation-Header"); + regattaAndRaceTimeInformationHeader = new FlowPanel(); + regattaAndRaceTimeInformationHeader.setStyleName("RegattaAndRaceTime-Header"); + timeRangeWithZoomModel = new TimeRangeWithZoomModel(); + componentViewers = new ArrayList(); + final CompetitorColorProvider colorProvider = new CompetitorColorProviderImpl(selectedRaceIdentifier, competitorsAndTheirBoats); + competitorSelectionProvider = new CompetitorSelectionModel(/* hasMultiSelection */ true, colorProvider); + + raceMapResources.combinedWindPanelStyle().ensureInjected(); + raceMap = new RaceMap(sailingService, asyncActionsExecutor, errorReporter, timer, + competitorSelectionProvider, stringMessages, showMapControls, getConfiguration().isShowViewStreamlets(), getConfiguration().isShowViewStreamletColors(), getConfiguration().isShowViewSimulation(), + selectedRaceIdentifier, raceMapResources.combinedWindPanelStyle(), /* showHeaderPanel */ true); + CompetitorFilterPanel competitorSearchTextBox = new CompetitorFilterPanel(competitorSelectionProvider, stringMessages, raceMap, + new LeaderboardFetcher() { + @Override + public LeaderboardDTO getLeaderboard() { + return leaderboardPanel.getLeaderboard(); + } + }, selectedRaceIdentifier); + raceMap.getLeftHeaderPanel().add(raceInformationHeader); + raceMap.getRightHeaderPanel().add(regattaAndRaceTimeInformationHeader); + + boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard = Document.get().getClientWidth() >= 1024; + leaderboardPanel = createLeaderboardPanel(leaderboardName, leaderboardGroupName, competitorSearchTextBox, isScreenLargeEnoughToInitiallyDisplayLeaderboard); + leaderboardPanel.setTitle(stringMessages.leaderboard()); + leaderboardPanel.getElement().getStyle().setMarginLeft(6, Unit.PX); + leaderboardPanel.getElement().getStyle().setMarginTop(10, Unit.PX); + createOneScreenView(leaderboardName, leaderboardGroupName, eventId, mainPanel, showMapControls, isScreenLargeEnoughToOfferChartSupport, isScreenLargeEnoughToInitiallyDisplayLeaderboard, raceMap, userService); // initializes the raceMap field + leaderboardPanel.addLeaderboardUpdateListener(this); + // in case the URL configuration contains the name of a competitors filter set we try to activate it + // FIXME the competitorsFilterSets has now moved to CompetitorSearchTextBox (which should probably be renamed); pass on the parameters to the LeaderboardPanel and see what it does with it + if (raceboardViewConfiguration.getActiveCompetitorsFilterSetName() != null) { + for (FilterSet> filterSet : competitorSearchTextBox.getCompetitorsFilterSets() + .getFilterSets()) { + if (filterSet.getName().equals(raceboardViewConfiguration.getActiveCompetitorsFilterSetName())) { + competitorSearchTextBox.getCompetitorsFilterSets().setActiveFilterSet(filterSet); + break; + } + } + } + timePanel = new RaceTimePanel(timer, timeRangeWithZoomModel, stringMessages, raceTimesInfoProvider, + raceboardViewConfiguration.isCanReplayDuringLiveRaces(), isScreenLargeEnoughToOfferChartSupport); + timeRangeWithZoomModel.addTimeZoomChangeListener(timePanel); + raceTimesInfoProvider.addRaceTimesInfoProviderListener(timePanel); + raceSelectionProvider.addRaceSelectionChangeListener(timePanel); + timePanel.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); + } + + /** + * @param event + * an optional event; may be null or else can be used to show some context information in + * the {@link GlobalNavigationPanel}. + * @param isScreenLargeEnoughToOfferChartSupport + * if the screen is large enough to display charts such as the competitor chart or the wind chart, a + * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and + * the charts are created. + * @param isScreenLargeEnoughToInitiallyDisplayLeaderboard TODO + */ + private void createOneScreenView(String leaderboardName, String leaderboardGroupName, UUID event, FlowPanel mainPanel, + boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport, boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard, RaceMap raceMap, UserService userService) { + // create the default leaderboard and select the right race + raceTimesInfoProvider.addRaceTimesInfoProviderListener(raceMap); + raceMap.onRaceSelectionChange(Collections.singletonList(selectedRaceIdentifier)); + List> components = new ArrayList>(); + if (isScreenLargeEnoughToOfferChartSupport) { + competitorChart = new MultiCompetitorRaceChart(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, + timer, timeRangeWithZoomModel, stringMessages, errorReporter, true, true, leaderboardGroupName, leaderboardName); + competitorChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); + competitorChart.getEntryWidget().setTitle(stringMessages.competitorCharts()); + competitorChart.setVisible(false); + components.add(competitorChart); + windChart = new WindChart(sailingService, raceSelectionProvider, timer, timeRangeWithZoomModel, new WindChartSettings(), + stringMessages, asyncActionsExecutor, errorReporter, /* compactChart */ true); + windChart.setVisible(false); + windChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); + windChart.getEntryWidget().setTitle(stringMessages.windChart()); + components.add(windChart); + editMarkPassingPanel = new EditMarkPassingsPanel(sailingService, selectedRaceIdentifier, + stringMessages, competitorSelectionProvider, errorReporter, timer); + editMarkPassingPanel.setLeaderboard(leaderboardPanel.getLeaderboard()); + editMarkPassingPanel.getEntryWidget().setTitle(stringMessages.editMarkPassings()); + components.add(editMarkPassingPanel); + } + boolean autoSelectMedia = getConfiguration().isAutoSelectMedia(); + MediaPlayerManagerComponent mediaPlayerManagerComponent = new MediaPlayerManagerComponent( + selectedRaceIdentifier, raceTimesInfoProvider, timer, mediaService, userService, stringMessages, + errorReporter, userAgent, this, autoSelectMedia); + leaderboardAndMapViewer = new SideBySideComponentViewer(leaderboardPanel, raceMap, mediaPlayerManagerComponent, components, stringMessages, userService, editMarkPassingPanel); + componentViewers.add(leaderboardAndMapViewer); + for (ComponentViewer componentViewer : componentViewers) { + mainPanel.add(componentViewer.getViewerWidget()); + } + boolean showLeaderboard = getConfiguration().isShowLeaderboard() && isScreenLargeEnoughToInitiallyDisplayLeaderboard; + setLeaderboardVisible(showLeaderboard); + if (isScreenLargeEnoughToOfferChartSupport) { + setWindChartVisible(getConfiguration().isShowWindChart()); + setCompetitorChartVisible(getConfiguration().isShowCompetitorsChart()); + } + // make sure to load leaderboard data for filtering to work + if (!showLeaderboard) { + leaderboardPanel.setVisible(true); + leaderboardPanel.setVisible(false); + } + } + + @SuppressWarnings("unused") + private void addSettingsMenuItem(MenuBar settingsMenu, final Component component) { + if (component.hasSettings()) { + settingsMenu.addItem(component.getLocalizedShortName(), new Command() { + public void execute() { + new SettingsDialog(component, stringMessages).show(); + } + }); + } + } + + private LeaderboardPanel createLeaderboardPanel(String leaderboardName, String leaderboardGroupName, + CompetitorFilterPanel competitorSearchTextBox, boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard) { + LeaderboardSettings leaderBoardSettings = LeaderboardSettingsFactory.getInstance() + .createNewSettingsForPlayMode(timer.getPlayMode(), + /* nameOfRaceToSort */ selectedRaceIdentifier.getRaceName(), + /* nameOfRaceColumnToShow */ null, /* nameOfRaceToShow */ selectedRaceIdentifier.getRaceName(), + new ExplicitRaceColumnSelectionWithPreselectedRace(selectedRaceIdentifier), /* showRegattaRank */ false, + /*showCompetitorSailIdColumn*/true, + /* don't showCompetitorFullNameColumn in case screen is so small that we don't + * even display the leaderboard initially */ isScreenLargeEnoughToInitiallyDisplayLeaderboard); + return new LeaderboardPanel(sailingService, asyncActionsExecutor, leaderBoardSettings, selectedRaceIdentifier != null, selectedRaceIdentifier, + competitorSelectionProvider, timer, leaderboardGroupName, leaderboardName, errorReporter, stringMessages, + userAgent, /* showRaceDetails */ true, competitorSearchTextBox, + /* showSelectionCheckbox */ true, raceTimesInfoProvider, /* autoExpandLastRaceColumn */ false, + /* don't adjust the timer's delay from the leaderboard; control it solely from the RaceTimesInfoProvider */ false, + /*autoApplyTopNFilter*/ false, /*showCompetitorFilterStatus*/ false); + } + + private void setComponentVisible(ComponentViewer componentViewer, Component component, boolean visible) { + component.setVisible(visible); + componentViewer.forceLayout(); + } + + /** + * Sets the collapsable panel for the leaderboard open or close, if in CASCADE view mode.
    + * Displays or hides the leaderboard, if in ONESCREEN view mode.

    + * + * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. + * + * @param visible true if the leaderboard shall be open/visible + */ + public void setLeaderboardVisible(boolean visible) { + setComponentVisible(leaderboardAndMapViewer, leaderboardPanel, visible); + } + + /** + * Sets the collapsable panel for the wind chart open or close, if in CASCADE view mode.
    + * Displays or hides the wind chart, if in ONESCREEN view mode.

    + * + * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. + * + * @param visible true if the wind chart shall be open/visible + */ + public void setWindChartVisible(boolean visible) { + setComponentVisible(leaderboardAndMapViewer, windChart, visible); + } + + /** + * Sets the collapsable panel for the competitor chart open or close, if in CASCADE view mode.
    + * Displays or hides the competitor chart, if in ONESCREEN view mode.

    + * + * The race board should be completely rendered before this method is called, or a few exceptions could be thrown. + * + * @param visible true if the competitor chart shall be open/visible + */ + public void setCompetitorChartVisible(boolean visible) { + setComponentVisible(leaderboardAndMapViewer, competitorChart, visible); + } + + public RaceTimePanel getTimePanel() { + return timePanel; + } + + protected SailingServiceAsync getSailingService() { + return sailingService; + } + + protected String getRaceBoardName() { + return raceBoardName; + } + + protected void setRaceBoardName(String raceBoardName) { + this.raceBoardName = raceBoardName; + } + + protected ErrorReporter getErrorReporter() { + return errorReporter; + } + + @Override + public void onRaceSelectionChange(List selectedRaces) { + } + + public RaceBoardViewConfiguration getConfiguration() { + return raceboardViewConfiguration; + } + + @Override + public void updatedLeaderboard(LeaderboardDTO leaderboard) { + leaderboardAndMapViewer.setLeftComponentWidth(leaderboardPanel.getContentPanel().getOffsetWidth()); + if (editMarkPassingPanel != null) { + editMarkPassingPanel.setLeaderboard(leaderboard); + } + } + + @Override + public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { + if (!currentRaceHasBeenSelectedOnce) { + FleetDTO fleet = raceColumn.getFleet(raceIdentifier); + String seriesName = raceColumn.getSeriesName(); + if (LeaderboardNameConstants.DEFAULT_SERIES_NAME.equals(seriesName)) { + seriesName = ""; + } + String fleetForRaceName = fleet==null?"":fleet.getName(); + if (fleetForRaceName.equals(LeaderboardNameConstants.DEFAULT_FLEET_NAME)) { + fleetForRaceName = ""; + } else { + fleetForRaceName = " - "+fleetForRaceName; + } + final Label raceNameLabel = new Label(stringMessages.race() + " " + raceColumn.getRaceColumnName()); + raceNameLabel.setStyleName("RaceName-Label"); + final Label raceAdditionalInformationLabel = new Label(seriesName + fleetForRaceName); + raceAdditionalInformationLabel.setStyleName("RaceSeriesAndFleet-Label"); + raceInformationHeader.clear(); + raceInformationHeader.add(raceNameLabel); + raceInformationHeader.add(raceAdditionalInformationLabel); + final Anchor regattaNameAnchor = new Anchor(raceIdentifier.getRegattaName()); + if (eventId != null) { + // we don't use the EntryPointLinkFactory here, because of lacking support for Places + String debugParam = Window.Location.getParameter("gwt.codesvr"); + String link = "/gwt/Home.html"; + if (debugParam != null && !debugParam.isEmpty()) { + link += "?gwt.codesvr=" + debugParam; + } + link += "#EventPlace:eventId="+eventId.toString(); + link += "&navigationTab=Regatta&leaderboardName=" + URLEncoder.encode(leaderboardName); + regattaNameAnchor.setHref(link); + } else { + String leaderboardGroupNameParam = Window.Location.getParameter("leaderboardGroupName"); + if(leaderboardGroupNameParam != null) { + Map leaderboardGroupLinkParameters = new HashMap(); + leaderboardGroupLinkParameters.put("showRaceDetails", "true"); + leaderboardGroupLinkParameters.put("leaderboardGroupName", leaderboardGroupNameParam); + String leaderBoardGroupLink = EntryPointLinkFactory.createLeaderboardGroupLink(leaderboardGroupLinkParameters); + regattaNameAnchor.setHref(leaderBoardGroupLink); + } else { + // fallback + regattaNameAnchor.setHref("javascript:window.history.back();"); + } + } + regattaNameAnchor.setStyleName("RegattaName-Anchor"); + Label raceTimeLabel = computeRaceInformation(raceColumn, fleet); + raceTimeLabel.setStyleName("RaceTime-Label"); + regattaAndRaceTimeInformationHeader.clear(); + regattaAndRaceTimeInformationHeader.add(regattaNameAnchor); + regattaAndRaceTimeInformationHeader.add(raceTimeLabel); + currentRaceHasBeenSelectedOnce = true; + } + } + + @Override + public UIObject getXPositionUiObject() { + return timePanel; + } + + @Override + public UIObject getYPositionUiObject() { + return timePanel; + } + + private Label computeRaceInformation(RaceColumnDTO raceColumn, FleetDTO fleet) { + Label raceInformationLabel = new Label(); + raceInformationLabel.setStyleName("Race-Time-Label"); + DateTimeFormat formatter = DateTimeFormat.getFormat("E d/M/y"); + raceInformationLabel.setText(formatter.format(raceColumn.getStartDate(fleet))); + return raceInformationLabel; + } + + @Override + public void onResize() { + Widget child = getWidget(); + if (child != null && child instanceof RequiresResize) { + ((RequiresResize) child).onResize(); + } + } +} + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java index 21091699f71..b16b50a7bc5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java @@ -1,302 +1,310 @@ -package com.sap.sailing.gwt.ui.raceboard; - -import java.util.ArrayList; -import java.util.List; - -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.ui.AbsolutePanel; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.DockLayoutPanel.Direction; -import com.google.gwt.user.client.ui.LayoutPanel; -import com.google.gwt.user.client.ui.Panel; -import com.google.gwt.user.client.ui.RequiresResize; -import com.google.gwt.user.client.ui.ScrollPanel; -import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.user.client.ui.WidgetCollection; -import com.sap.sailing.domain.common.security.Roles; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.media.MediaManagementControl; -import com.sap.sailing.gwt.ui.client.media.MediaPlayerManager; -import com.sap.sailing.gwt.ui.client.media.MediaPlayerManager.PlayerChangeListener; -import com.sap.sailing.gwt.ui.client.media.MediaPlayerManagerComponent; -import com.sap.sailing.gwt.ui.client.media.MediaSingleSelectionControl; -import com.sap.sailing.gwt.ui.client.shared.charts.EditMarkPassingsPanel; -import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; -import com.sap.sailing.gwt.ui.raceboard.TouchSplitLayoutPanel.Splitter; -import com.sap.sse.common.Util.Pair; -import com.sap.sse.common.settings.AbstractSettings; -import com.sap.sse.gwt.client.shared.components.Component; -import com.sap.sse.gwt.client.shared.components.ComponentViewer; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.security.shared.DefaultRoles; -import com.sap.sse.security.ui.client.UserService; -import com.sap.sse.security.ui.client.UserStatusEventHandler; -import com.sap.sse.security.ui.shared.UserDTO; - -/** - * Component Viewer that uses a {@link TouchSplitLayoutPanel} to display its components. - * - * TODO: Refactor to make sure it is really only performing operations that are related to view. Currently it is digging - * too deep into components and setting titles or even creating video buttons. - */ -public class SideBySideComponentViewer implements ComponentViewer, UserStatusEventHandler { - - private static final int DEFAULT_SOUTH_SPLIT_PANEL_HEIGHT = 200; - private final int MIN_LEADERBOARD_WIDTH = 432; // works well for 505 and ESS - - /** - * Absolute Panel that informs its children about a resize - */ - class ResizableAbsolutePanel extends AbsolutePanel implements RequiresResize { - public void onResize() { - WidgetCollection children = getChildren(); - for (Widget widget : children) { - if (widget instanceof RequiresResize) { - ((RequiresResize) widget).onResize(); - } - } - } - } - - private final LeaderboardPanel leftComponent; - private final Component rightComponent; - private final List> components; - private final ScrollPanel leftScrollPanel; - private final StringMessages stringMessages; - private final Button mediaSelectionButton; - private final Button mediaManagementButton; - private final EditMarkPassingsPanel markPassingsPanel; - - private LayoutPanel mainPanel; - - private TouchSplitLayoutPanel splitLayoutPanel; - private int savedSplitPosition = -1; - private boolean layoutForLeftComponentForcedOnce = false; - - public SideBySideComponentViewer(final LeaderboardPanel leftComponentP, final Component rightComponentP, - final MediaPlayerManagerComponent mediaPlayerManagerComponent, List> components, - final StringMessages stringMessages, UserService userService, EditMarkPassingsPanel markPassingsPanel) { - this.stringMessages = stringMessages; - this.leftComponent = leftComponentP; - this.rightComponent = rightComponentP; - this.components = components; - this.mediaSelectionButton = createMediaSelectionButton(mediaPlayerManagerComponent); - this.mediaManagementButton = createMediaManagementButton(mediaPlayerManagerComponent); - this.markPassingsPanel = markPassingsPanel; - userService.addUserStatusEventHandler(this); - mediaPlayerManagerComponent.setPlayerChangeListener(new PlayerChangeListener() { - public void notifyStateChange() { - String caption; - String tooltip; - switch (mediaPlayerManagerComponent.getAssignedMediaTracks().size()) { - case 0: - caption = stringMessages.mediaNoVideosCaption(); - tooltip = caption; - mediaSelectionButton.setVisible(false); - break; - case 1: - mediaSelectionButton.setVisible(true); - if(mediaPlayerManagerComponent.isPlaying()){ - caption = stringMessages.mediaHideVideoCaption(); - tooltip = stringMessages.mediaHideVideoTooltip(); - } - else{ - caption = stringMessages.mediaShowVideoCaption(); - tooltip = stringMessages.mediaShowVideoTooltip(mediaPlayerManagerComponent.getAssignedMediaTracks().iterator().next().title); - } - break; - default: - mediaSelectionButton.setVisible(true); - caption = stringMessages.mediaSelectVideoCaption(mediaPlayerManagerComponent.getAssignedMediaTracks().size()); - tooltip = stringMessages.mediaSelectVideoTooltip(); - break; - } - if (mediaPlayerManagerComponent.isPlaying()) { - mediaSelectionButton.addStyleDependentName("mediaplaying"); - } else { - mediaSelectionButton.removeStyleDependentName("mediaplaying"); - } - mediaSelectionButton.setText(caption); - mediaSelectionButton.setTitle(tooltip); - mediaManagementButton.setVisible(mediaPlayerManagerComponent.allowsEditing()); - } - }); - this.leftScrollPanel = new ScrollPanel(); - this.leftScrollPanel.add(leftComponentP.getEntryWidget()); - this.leftScrollPanel.setTitle(leftComponentP.getEntryWidget().getTitle()); - this.mainPanel = new LayoutPanel(); - this.mainPanel.setSize("100%", "100%"); - this.mainPanel.getElement().getStyle().setMarginTop(-12, Unit.PX); - this.mainPanel.setStyleName("SideBySideComponentViewer-MainPanel"); - this.splitLayoutPanel = new TouchSplitLayoutPanel(/* horizontal splitter width */3, /* vertical splitter height */ 25); - this.mainPanel.add(splitLayoutPanel); - - // initialize components - they need to be added before other widgets to get the right width - initializeComponents(); - - // initialize the leaderboard component - savedSplitPosition = MIN_LEADERBOARD_WIDTH; - splitLayoutPanel.insert(leftScrollPanel, leftComponent, Direction.WEST, savedSplitPosition); - - // create a panel that will contain the horizontal toggle buttons - ResizableAbsolutePanel panelForMapAndHorizontalToggleButtons = new ResizableAbsolutePanel(); - panelForMapAndHorizontalToggleButtons.add(rightComponent.getEntryWidget()); - splitLayoutPanel.insert(panelForMapAndHorizontalToggleButtons, rightComponent, Direction.CENTER, 0); - - // add additional toggle buttons panel that currently only contains the video button - List> additionalVerticalButtons = new ArrayList>(); - additionalVerticalButtons.add(new Pair(mediaSelectionButton, - mediaPlayerManagerComponent.getDependentCssClassName())); - if (/* TODO check for correct role; was: user != null */ true) { - additionalVerticalButtons.add(new Pair(mediaManagementButton, - "managemedia")); - } - onUserStatusChange(userService.getCurrentUser()); - // ensure that toggle buttons are positioned right - splitLayoutPanel.lastComponentHasBeenAdded(this, panelForMapAndHorizontalToggleButtons, - additionalVerticalButtons); - } - - /** - * Create the video control button that shows or hides the video popup - * - * @param userAgent - */ - private Button createMediaSelectionButton(final MediaPlayerManager mediaPlayerManager) { - final Button result = new Button(); - final MediaSingleSelectionControl multiSelectionControl = new MediaSingleSelectionControl(mediaPlayerManager, - result, stringMessages); - result.addClickHandler(new ClickHandler() { - - @Override - public void onClick(ClickEvent event) { - if (mediaPlayerManager.getAssignedMediaTracks().size() == 1) { - if (mediaPlayerManager.isPlaying()) { - mediaPlayerManager.stopAll(); - } else { - mediaPlayerManager.playDefault(); - - } - } else { - multiSelectionControl.show(); - } - } - }); - - // hide button initially as we defer showing the button to the asynchroneous - // task that gets launched by the media service to get video tracks - result.setVisible(false); - return result; - } - - /** - * Create the video control button that shows or hides the video popup - * - * @param userAgent - */ - private Button createMediaManagementButton(final MediaPlayerManager mediaPlayerManager) { - final Button result = new Button(stringMessages.mediaManageMediaCaption()); - result.setTitle(stringMessages.mediaManageMediaTooltip()); - // onClick - final MediaManagementControl multiSelectionControl = new MediaManagementControl(mediaPlayerManager, - result, stringMessages); - result.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - multiSelectionControl.show(); - } - }); - // hide button initially as we defer showing the button to the asynchronous - // task that gets launched by the media service to get video tracks - result.setVisible(false); - return result; - } - - private void initializeComponents() { - for (final Component component : components) { - splitLayoutPanel.insert(component.getEntryWidget(), component, Direction.SOUTH, 200); - } - } - - public void showSettingsDialog(Component component) { - if (component.hasSettings()) { - new SettingsDialog(component, stringMessages).show(); - } - } - - /** - * Called whenever the layout of {@link TouchSplitLayoutPanel} or other components change. Controls the visibility - * based on the {@link Component}s visibility. Each {@link Component} is in charge to not display any data or update - * itself when it is not visible. - */ - public void forceLayout() { - if (!leftComponent.isVisible() && rightComponent.isVisible()) { - // the leaderboard is not visible, but the map is - if (isWidgetInSplitPanel(leftScrollPanel)) { - if (leftScrollPanel.getOffsetWidth() > 0) { - savedSplitPosition = leftScrollPanel.getOffsetWidth(); - } - splitLayoutPanel.setWidgetVisibility(leftScrollPanel, leftComponent, /* hidden */true, - savedSplitPosition); - } - } else if (leftComponent.isVisible() && rightComponent.isVisible()) { - // the leaderboard and the map are visible - splitLayoutPanel.setWidgetVisibility(leftScrollPanel, leftComponent, /* hidden */false, savedSplitPosition); - } else if (!leftComponent.isVisible() && !rightComponent.isVisible()) { - } - - for (Component component : components) { - final boolean isComponentVisible = component.isVisible(); - splitLayoutPanel.setWidgetVisibility(component.getEntryWidget(), component, !isComponentVisible, - DEFAULT_SOUTH_SPLIT_PANEL_HEIGHT); - } - splitLayoutPanel.forceLayout(); - } - - private boolean isWidgetInSplitPanel(Widget widget) { - int widgetIndex = splitLayoutPanel.getWidgetIndex(widget); - return widgetIndex >= 0; - } - - public Panel getViewerWidget() { - return mainPanel; - } - - public Component getRootComponent() { - return null; - } - - public String getViewerName() { - return ""; - } - - public void setLeftComponentWidth(int width) { - // TODO: The information provided by width is wrong - // need to find a way to get the correct information - if (!layoutForLeftComponentForcedOnce) { - savedSplitPosition = MIN_LEADERBOARD_WIDTH; - forceLayout(); - } - layoutForLeftComponentForcedOnce = true; - } - - @Override - public void onUserStatusChange(UserDTO user) { - final Splitter associatedSplitter = splitLayoutPanel.getAssociatedSplitter(markPassingsPanel); - if (associatedSplitter != null) { // if the panel is not present, the splitter may not be found - final Button toggleButton = associatedSplitter.getToggleButton(); - if (user != null - && (user.hasRole(DefaultRoles.ADMIN.getRolename()) || - user.hasRole(Roles.eventmanager.getRolename()))) { - toggleButton.setVisible(true); - forceLayout(); - } else { - markPassingsPanel.setVisible(false); - toggleButton.setVisible(false); - forceLayout(); - } - } - } -} +package com.sap.sailing.gwt.ui.raceboard; + +import java.util.ArrayList; +import java.util.List; + +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.AbsolutePanel; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.DockLayoutPanel.Direction; +import com.google.gwt.user.client.ui.LayoutPanel; +import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.user.client.ui.RequiresResize; +import com.google.gwt.user.client.ui.ScrollPanel; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.user.client.ui.WidgetCollection; +import com.sap.sailing.domain.common.security.Roles; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.client.media.MediaManagementControl; +import com.sap.sailing.gwt.ui.client.media.MediaPlayerManager; +import com.sap.sailing.gwt.ui.client.media.MediaPlayerManager.PlayerChangeListener; +import com.sap.sailing.gwt.ui.client.media.MediaPlayerManagerComponent; +import com.sap.sailing.gwt.ui.client.media.MediaSingleSelectionControl; +import com.sap.sailing.gwt.ui.client.shared.charts.EditMarkPassingsPanel; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; +import com.sap.sailing.gwt.ui.raceboard.TouchSplitLayoutPanel.Splitter; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.settings.AbstractSettings; +import com.sap.sse.gwt.client.shared.components.Component; +import com.sap.sse.gwt.client.shared.components.ComponentViewer; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.security.shared.DefaultRoles; +import com.sap.sse.security.ui.client.UserService; +import com.sap.sse.security.ui.client.UserStatusEventHandler; +import com.sap.sse.security.ui.shared.UserDTO; + +/** + * Component Viewer that uses a {@link TouchSplitLayoutPanel} to display its components. + * + * TODO: Refactor to make sure it is really only performing operations that are related to view. Currently it is digging + * too deep into components and setting titles or even creating video buttons. + */ +public class SideBySideComponentViewer implements ComponentViewer, UserStatusEventHandler { + + private static final int DEFAULT_SOUTH_SPLIT_PANEL_HEIGHT = 200; + private final int MIN_LEADERBOARD_WIDTH = Math.min(432, Window.getClientWidth() - 40); // works well for 505 and ESS + + /** + * Absolute Panel that informs its children about a resize + */ + class ResizableAbsolutePanel extends AbsolutePanel implements RequiresResize { + public void onResize() { + WidgetCollection children = getChildren(); + for (Widget widget : children) { + if (widget instanceof RequiresResize) { + ((RequiresResize) widget).onResize(); + } + } + } + } + + private final LeaderboardPanel leftComponent; + private final Component rightComponent; + private final List> components; + private final ScrollPanel leftScrollPanel; + private final StringMessages stringMessages; + private final Button mediaSelectionButton; + private final Button mediaManagementButton; + private final EditMarkPassingsPanel markPassingsPanel; + + private LayoutPanel mainPanel; + + private TouchSplitLayoutPanel splitLayoutPanel; + private int savedSplitPosition = -1; + private boolean layoutForLeftComponentForcedOnce = false; + + public SideBySideComponentViewer(final LeaderboardPanel leftComponentP, final Component rightComponentP, + final MediaPlayerManagerComponent mediaPlayerManagerComponent, List> components, + final StringMessages stringMessages, UserService userService, EditMarkPassingsPanel markPassingsPanel) { + this.stringMessages = stringMessages; + this.leftComponent = leftComponentP; + this.rightComponent = rightComponentP; + this.components = components; + this.mediaSelectionButton = createMediaSelectionButton(mediaPlayerManagerComponent); + this.mediaManagementButton = createMediaManagementButton(mediaPlayerManagerComponent); + this.markPassingsPanel = markPassingsPanel; + userService.addUserStatusEventHandler(this); + mediaPlayerManagerComponent.setPlayerChangeListener(new PlayerChangeListener() { + public void notifyStateChange() { + String caption; + String tooltip; + switch (mediaPlayerManagerComponent.getAssignedMediaTracks().size()) { + case 0: + caption = stringMessages.mediaNoVideosCaption(); + tooltip = caption; + mediaSelectionButton.setVisible(false); + break; + case 1: + mediaSelectionButton.setVisible(true); + if(mediaPlayerManagerComponent.isPlaying()){ + caption = stringMessages.mediaHideVideoCaption(); + tooltip = stringMessages.mediaHideVideoTooltip(); + } + else{ + caption = stringMessages.mediaShowVideoCaption(); + tooltip = stringMessages.mediaShowVideoTooltip(mediaPlayerManagerComponent.getAssignedMediaTracks().iterator().next().title); + } + break; + default: + mediaSelectionButton.setVisible(true); + caption = stringMessages.mediaSelectVideoCaption(mediaPlayerManagerComponent.getAssignedMediaTracks().size()); + tooltip = stringMessages.mediaSelectVideoTooltip(); + break; + } + if (mediaPlayerManagerComponent.isPlaying()) { + mediaSelectionButton.addStyleDependentName("mediaplaying"); + } else { + mediaSelectionButton.removeStyleDependentName("mediaplaying"); + } + mediaSelectionButton.setText(caption); + mediaSelectionButton.setTitle(tooltip); + mediaManagementButton.setVisible(mediaPlayerManagerComponent.allowsEditing()); + } + }); + this.leftScrollPanel = new ScrollPanel(); + this.leftScrollPanel.add(leftComponentP.getEntryWidget()); + this.leftScrollPanel.setTitle(leftComponentP.getEntryWidget().getTitle()); + this.mainPanel = new LayoutPanel() { + @Override + public void onResize() { + savedSplitPosition = Math.min(leftScrollPanel.getOffsetWidth(), Window.getClientWidth() - 40); + splitLayoutPanel.setWidgetSize(leftScrollPanel, savedSplitPosition); + super.onResize(); + } + }; + this.mainPanel.setSize("100%", "100%"); + this.mainPanel.getElement().getStyle().setMarginTop(-12, Unit.PX); + this.mainPanel.setStyleName("SideBySideComponentViewer-MainPanel"); + this.splitLayoutPanel = new TouchSplitLayoutPanel(/* horizontal splitter width */3, /* vertical splitter height */ 25); + this.mainPanel.add(splitLayoutPanel); + + // initialize components - they need to be added before other widgets to get the right width + initializeComponents(); + + // initialize the leaderboard component + savedSplitPosition = MIN_LEADERBOARD_WIDTH; + splitLayoutPanel.insert(leftScrollPanel, leftComponent, Direction.WEST, savedSplitPosition); + + // create a panel that will contain the horizontal toggle buttons + ResizableAbsolutePanel panelForMapAndHorizontalToggleButtons = new ResizableAbsolutePanel(); + panelForMapAndHorizontalToggleButtons.add(rightComponent.getEntryWidget()); + splitLayoutPanel.insert(panelForMapAndHorizontalToggleButtons, rightComponent, Direction.CENTER, 0); + + // add additional toggle buttons panel that currently only contains the video button + List> additionalVerticalButtons = new ArrayList>(); + additionalVerticalButtons.add(new Pair(mediaSelectionButton, + mediaPlayerManagerComponent.getDependentCssClassName())); + if (/* TODO check for correct role; was: user != null */ true) { + additionalVerticalButtons.add(new Pair(mediaManagementButton, + "managemedia")); + } + onUserStatusChange(userService.getCurrentUser()); + // ensure that toggle buttons are positioned right + splitLayoutPanel.lastComponentHasBeenAdded(this, panelForMapAndHorizontalToggleButtons, + additionalVerticalButtons); + } + + /** + * Create the video control button that shows or hides the video popup + * + * @param userAgent + */ + private Button createMediaSelectionButton(final MediaPlayerManager mediaPlayerManager) { + final Button result = new Button(); + final MediaSingleSelectionControl multiSelectionControl = new MediaSingleSelectionControl(mediaPlayerManager, + result, stringMessages); + result.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + if (mediaPlayerManager.getAssignedMediaTracks().size() == 1) { + if (mediaPlayerManager.isPlaying()) { + mediaPlayerManager.stopAll(); + } else { + mediaPlayerManager.playDefault(); + + } + } else { + multiSelectionControl.show(); + } + } + }); + + // hide button initially as we defer showing the button to the asynchroneous + // task that gets launched by the media service to get video tracks + result.setVisible(false); + return result; + } + + /** + * Create the video control button that shows or hides the video popup + * + * @param userAgent + */ + private Button createMediaManagementButton(final MediaPlayerManager mediaPlayerManager) { + final Button result = new Button(stringMessages.mediaManageMediaCaption()); + result.setTitle(stringMessages.mediaManageMediaTooltip()); + // onClick + final MediaManagementControl multiSelectionControl = new MediaManagementControl(mediaPlayerManager, + result, stringMessages); + result.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + multiSelectionControl.show(); + } + }); + // hide button initially as we defer showing the button to the asynchronous + // task that gets launched by the media service to get video tracks + result.setVisible(false); + return result; + } + + private void initializeComponents() { + for (final Component component : components) { + splitLayoutPanel.insert(component.getEntryWidget(), component, Direction.SOUTH, 200); + } + } + + public void showSettingsDialog(Component component) { + if (component.hasSettings()) { + new SettingsDialog(component, stringMessages).show(); + } + } + + /** + * Called whenever the layout of {@link TouchSplitLayoutPanel} or other components change. Controls the visibility + * based on the {@link Component}s visibility. Each {@link Component} is in charge to not display any data or update + * itself when it is not visible. + */ + public void forceLayout() { + if (!leftComponent.isVisible() && rightComponent.isVisible()) { + // the leaderboard is not visible, but the map is + if (isWidgetInSplitPanel(leftScrollPanel)) { + if (leftScrollPanel.getOffsetWidth() > 0) { + savedSplitPosition = Math.min(savedSplitPosition, leftScrollPanel.getOffsetWidth()); + } + splitLayoutPanel.setWidgetVisibility(leftScrollPanel, leftComponent, /* hidden */true, + savedSplitPosition); + } + } else if (leftComponent.isVisible() && rightComponent.isVisible()) { + // the leaderboard and the map are visible + splitLayoutPanel.setWidgetVisibility(leftScrollPanel, leftComponent, /* hidden */false, savedSplitPosition); + } else if (!leftComponent.isVisible() && !rightComponent.isVisible()) { + } + + for (Component component : components) { + final boolean isComponentVisible = component.isVisible(); + splitLayoutPanel.setWidgetVisibility(component.getEntryWidget(), component, !isComponentVisible, + DEFAULT_SOUTH_SPLIT_PANEL_HEIGHT); + } + splitLayoutPanel.forceLayout(); + } + + private boolean isWidgetInSplitPanel(Widget widget) { + int widgetIndex = splitLayoutPanel.getWidgetIndex(widget); + return widgetIndex >= 0; + } + + public Panel getViewerWidget() { + return mainPanel; + } + + public Component getRootComponent() { + return null; + } + + public String getViewerName() { + return ""; + } + + public void setLeftComponentWidth(int width) { + // TODO: The information provided by width is wrong + // need to find a way to get the correct information + if (!layoutForLeftComponentForcedOnce) { + savedSplitPosition = MIN_LEADERBOARD_WIDTH; + forceLayout(); + } + layoutForLeftComponentForcedOnce = true; + } + + @Override + public void onUserStatusChange(UserDTO user) { + final Splitter associatedSplitter = splitLayoutPanel.getAssociatedSplitter(markPassingsPanel); + if (associatedSplitter != null) { // if the panel is not present, the splitter may not be found + final Button toggleButton = associatedSplitter.getToggleButton(); + if (user != null + && (user.hasRole(DefaultRoles.ADMIN.getRolename()) || + user.hasRole(Roles.eventmanager.getRolename()))) { + toggleButton.setVisible(true); + forceLayout(); + } else { + markPassingsPanel.setVisible(false); + toggleButton.setVisible(false); + forceLayout(); + } + } + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java index dfcff9fd58f..fed18802b88 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java @@ -842,6 +842,24 @@ public class TouchSplitLayoutPanel extends DockLayoutPanel { } } } + + /** + * Sets the size of the given {@link Widget} if it is {@link Widget#isVisible() visible}. Otherwise, the given size + * will be persisted as the widgets {@link LayoutData#oldSize old size}. + * + * @param widget the {@link Widget} to resize + * @param size the new size + */ + public void setWidgetSize(Widget widget, int size) { + final Splitter splitter = getAssociatedSplitter(widget); + if (splitter != null) { + if (widget.isVisible()) { + splitter.setAssociatedWidgetSize(size, /* defer */false); + } else { + ((LayoutData) widget.getLayoutData()).oldSize = size; + } + } + } private void assertIsChild2(Widget widget) { assert (widget == null) || (widget.getParent() == this) : "The specified widget is not a child of this panel"; From ffb2e46d0fade55894bf4c3d01f487cb1c33435c Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Mon, 23 Nov 2015 14:49:33 +0100 Subject: [PATCH 030/306] Fixed resizing of RaceBoards timepanel when changing browser window size or mobile device orientation --- .../gwt/ui/raceboard/RaceBoardEntryPoint.java | 402 +++++++++--------- 1 file changed, 201 insertions(+), 201 deletions(-) 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 3e95fa2d33a..11658613ec0 100755 --- 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,201 +1,201 @@ - -package com.sap.sailing.gwt.ui.raceboard; - -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -import com.google.gwt.core.client.GWT; -import com.google.gwt.dom.client.Document; -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.rpc.AsyncCallback; -import com.google.gwt.user.client.rpc.ServiceDefTarget; -import com.google.gwt.user.client.ui.Button; -import com.google.gwt.user.client.ui.DockLayoutPanel; -import com.google.gwt.user.client.ui.FlowPanel; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.RootLayoutPanel; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -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.RaceSelectionModel; -import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; -import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; -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.shared.GwtHttpRequestUtils; - -public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { - private RaceWithCompetitorsDTO selectedRace; - - private static final String PARAM_REGATTA_NAME = "regattaName"; - private static final String PARAM_RACE_NAME = "raceName"; - private static final String PARAM_LEADERBOARD_NAME = "leaderboardName"; - private static final String PARAM_LEADERBOARD_GROUP_NAME = "leaderboardGroupName"; - private static final String PARAM_EVENT_ID = "eventId"; - - private String regattaName; - private String raceName; - private String leaderboardName; - private String leaderboardGroupName; - private UUID eventId; - private RaceBoardViewConfiguration raceboardViewConfig; - - private final MediaServiceAsync mediaService = GWT.create(MediaService.class); - - @Override - protected void doOnModuleLoad() { - super.doOnModuleLoad(); - EntryPointHelper.registerASyncService((ServiceDefTarget) mediaService, RemoteServiceMappingConstants.mediaServiceRemotePath); - // read mandatory parameters - regattaName = Window.Location.getParameter(PARAM_REGATTA_NAME); - raceName = Window.Location.getParameter(PARAM_RACE_NAME); - String leaderboardNameParamValue = Window.Location.getParameter(PARAM_LEADERBOARD_NAME); - String leaderboardGroupNameParamValue = Window.Location.getParameter(PARAM_LEADERBOARD_GROUP_NAME); - if (leaderboardNameParamValue != null && !leaderboardNameParamValue.isEmpty()) { - leaderboardName = leaderboardNameParamValue; - } - if (leaderboardGroupNameParamValue != null && !leaderboardGroupNameParamValue.isEmpty()) { - leaderboardGroupName = leaderboardGroupNameParamValue; - } - String eventIdParamValue = Window.Location.getParameter(PARAM_EVENT_ID); - if (eventIdParamValue != null && !eventIdParamValue.isEmpty()) { - eventId = UUID.fromString(eventIdParamValue); - } - if (leaderboardGroupNameParamValue != null && !leaderboardGroupNameParamValue.isEmpty()) { - leaderboardGroupName = leaderboardGroupNameParamValue; - } - if (regattaName == null || regattaName.isEmpty() || raceName == null || raceName.isEmpty() || - leaderboardName == null || leaderboardName.isEmpty()) { - createErrorPage("This page requires a valid regatta name, race name and leaderboard name."); - return; - } - - // read optional parameters - boolean showLeaderboard = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_LEADERBOARD, true /* default*/); - boolean showWindChart = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_WINDCHART, false /* default*/); - boolean showCompetitorsChart = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_COMPETITORSCHART, false /* default*/); - boolean showViewStreamlets = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_STREAMLETS, false /* default*/); - boolean showViewStreamletColors = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_STREAMLET_COLORS, false /* default*/); - boolean showViewSimulation = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_SIMULATION, true /* default*/); - String activeCompetitorsFilterSetName = GwtHttpRequestUtils.getStringParameter(RaceBoardViewConfiguration.PARAM_VIEW_COMPETITOR_FILTER, null /* default*/); - final boolean canReplayWhileLiveIsPossible = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_CAN_REPLAY_DURING_LIVE_RACES, false); - final boolean autoSelectMedia = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_AUTOSELECT_MEDIA, true); - final String defaultMedia = GwtHttpRequestUtils.getStringParameter(RaceBoardViewConfiguration.PARAM_DEFAULT_MEDIA, null /* default*/); - final boolean showMapControls = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_MAPCONTROLS, true /* default*/); - raceboardViewConfig = new RaceBoardViewConfiguration(activeCompetitorsFilterSetName, showLeaderboard, - showWindChart, showCompetitorsChart, showViewStreamlets, showViewStreamletColors, showViewSimulation, canReplayWhileLiveIsPossible, autoSelectMedia, defaultMedia); - - sailingService.getRaceboardData(regattaName, raceName, leaderboardName, leaderboardGroupName, eventId, new AsyncCallback() { - @Override - public void onSuccess(RaceboardDataDTO result) { - checkUrlParameters(result, canReplayWhileLiveIsPossible, showMapControls, - /* isScreenLargeEnoughToOfferChartSupport */ Document.get().getClientHeight() >= 600); - } - - @Override - public void onFailure(Throwable caught) { - reportError("Error trying to create the raceboard: " + caught.getMessage()); - } - }); - } - - 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 void checkUrlParameters(RaceboardDataDTO raceboardData, boolean canReplayWhileLiveIsPossible, boolean showMapControls, - boolean isScreenLargeEnoughToOfferChartSupport) { - if (!raceboardData.isValidLeaderboard()) { - createErrorPage(getStringMessages().noSuchLeaderboard()); - return; - } - if (eventId != null && !raceboardData.isValidEvent()) { - createErrorPage(getStringMessages().noSuchEvent()); - } - if (leaderboardGroupName != null) { - if(!raceboardData.isValidLeaderboardGroup()) { - createErrorPage(getStringMessages().leaderboardNotContainedInLeaderboardGroup(leaderboardName, leaderboardGroupName)); - return; - } - if (eventId != null && raceboardData.isValidLeaderboardGroup() && !raceboardData.isValidEvent()) { - createErrorPage(getStringMessages().leaderboardGroupNotContainedInEvent(leaderboardGroupName, eventId.toString())); - return; - } - } - if (raceboardData.getRace() == null) { - createErrorPage("Could not obtain a race with name " + raceName + " for a regatta with name " + regattaName); - return; - } - selectedRace = raceboardData.getRace(); - Window.setTitle(selectedRace.getName()); - RaceSelectionModel raceSelectionModel = new RaceSelectionModel(); - List singletonList = Collections.singletonList(selectedRace.getRaceIdentifier()); - raceSelectionModel.setSelection(singletonList); - Timer timer = new Timer(PlayModes.Replay, 1000l); - AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); - RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, asyncActionsExecutor, this, singletonList, 5000l /* requestInterval*/); - RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, getUserService(), asyncActionsExecutor, - raceboardData.getCompetitorAndTheirBoats(), timer, raceSelectionModel, leaderboardName, leaderboardGroupName, eventId, - raceboardViewConfig, RaceBoardEntryPoint.this, getStringMessages(), userAgent, raceTimesInfoProvider, showMapControls, - isScreenLargeEnoughToOfferChartSupport); - - createRaceBoardInOneScreenMode(raceBoardPanel, raceboardViewConfig); - } - - private FlowPanel createTimePanel(RaceBoardPanel raceBoardPanel) { - FlowPanel timeLineInnerBgPanel = new FlowPanel(); - timeLineInnerBgPanel.addStyleName("timeLineInnerBgPanel"); - timeLineInnerBgPanel.add(raceBoardPanel.getTimePanel()); - - FlowPanel timeLineInnerPanel = new FlowPanel(); - timeLineInnerPanel.add(timeLineInnerBgPanel); - timeLineInnerPanel.addStyleName("timeLineInnerPanel"); - - FlowPanel timelinePanel = new FlowPanel(); - timelinePanel.add(timeLineInnerPanel); - timelinePanel.addStyleName("timeLinePanel"); - - return timelinePanel; - } - - private void createRaceBoardInOneScreenMode(final RaceBoardPanel raceBoardPanel, - RaceBoardViewConfiguration raceboardViewConfiguration) { - final DockLayoutPanel p = new DockLayoutPanel(Unit.PX); - RootLayoutPanel.get().add(p); - final FlowPanel timePanel = createTimePanel(raceBoardPanel); - final Button toggleButton = raceBoardPanel.getTimePanel().getAdvancedToggleButton(); - toggleButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - boolean advancedModeShown = raceBoardPanel.getTimePanel().toggleAdvancedMode(); - if (advancedModeShown) { - p.setWidgetSize(timePanel, 96); - toggleButton.removeStyleDependentName("Closed"); - toggleButton.addStyleDependentName("Open"); - } else { - p.setWidgetSize(timePanel, 67); - toggleButton.addStyleDependentName("Closed"); - toggleButton.removeStyleDependentName("Open"); - } - } - }); - p.addSouth(timePanel, 67); - p.add(raceBoardPanel); - p.addStyleName("dockLayoutPanel"); - } -} + +package com.sap.sailing.gwt.ui.raceboard; + +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.Document; +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.rpc.AsyncCallback; +import com.google.gwt.user.client.rpc.ServiceDefTarget; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.DockLayoutPanel; +import com.google.gwt.user.client.ui.FlowPanel; +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.RootLayoutPanel; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +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.RaceSelectionModel; +import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; +import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; +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.shared.GwtHttpRequestUtils; + +public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { + private RaceWithCompetitorsDTO selectedRace; + + private static final String PARAM_REGATTA_NAME = "regattaName"; + private static final String PARAM_RACE_NAME = "raceName"; + private static final String PARAM_LEADERBOARD_NAME = "leaderboardName"; + private static final String PARAM_LEADERBOARD_GROUP_NAME = "leaderboardGroupName"; + private static final String PARAM_EVENT_ID = "eventId"; + + private String regattaName; + private String raceName; + private String leaderboardName; + private String leaderboardGroupName; + private UUID eventId; + private RaceBoardViewConfiguration raceboardViewConfig; + + private final MediaServiceAsync mediaService = GWT.create(MediaService.class); + + @Override + protected void doOnModuleLoad() { + super.doOnModuleLoad(); + EntryPointHelper.registerASyncService((ServiceDefTarget) mediaService, RemoteServiceMappingConstants.mediaServiceRemotePath); + // read mandatory parameters + regattaName = Window.Location.getParameter(PARAM_REGATTA_NAME); + raceName = Window.Location.getParameter(PARAM_RACE_NAME); + String leaderboardNameParamValue = Window.Location.getParameter(PARAM_LEADERBOARD_NAME); + String leaderboardGroupNameParamValue = Window.Location.getParameter(PARAM_LEADERBOARD_GROUP_NAME); + if (leaderboardNameParamValue != null && !leaderboardNameParamValue.isEmpty()) { + leaderboardName = leaderboardNameParamValue; + } + if (leaderboardGroupNameParamValue != null && !leaderboardGroupNameParamValue.isEmpty()) { + leaderboardGroupName = leaderboardGroupNameParamValue; + } + String eventIdParamValue = Window.Location.getParameter(PARAM_EVENT_ID); + if (eventIdParamValue != null && !eventIdParamValue.isEmpty()) { + eventId = UUID.fromString(eventIdParamValue); + } + if (leaderboardGroupNameParamValue != null && !leaderboardGroupNameParamValue.isEmpty()) { + leaderboardGroupName = leaderboardGroupNameParamValue; + } + if (regattaName == null || regattaName.isEmpty() || raceName == null || raceName.isEmpty() || + leaderboardName == null || leaderboardName.isEmpty()) { + createErrorPage("This page requires a valid regatta name, race name and leaderboard name."); + return; + } + + // read optional parameters + boolean showLeaderboard = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_LEADERBOARD, true /* default*/); + boolean showWindChart = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_WINDCHART, false /* default*/); + boolean showCompetitorsChart = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_COMPETITORSCHART, false /* default*/); + boolean showViewStreamlets = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_STREAMLETS, false /* default*/); + boolean showViewStreamletColors = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_STREAMLET_COLORS, false /* default*/); + boolean showViewSimulation = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_SIMULATION, true /* default*/); + String activeCompetitorsFilterSetName = GwtHttpRequestUtils.getStringParameter(RaceBoardViewConfiguration.PARAM_VIEW_COMPETITOR_FILTER, null /* default*/); + final boolean canReplayWhileLiveIsPossible = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_CAN_REPLAY_DURING_LIVE_RACES, false); + final boolean autoSelectMedia = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_AUTOSELECT_MEDIA, true); + final String defaultMedia = GwtHttpRequestUtils.getStringParameter(RaceBoardViewConfiguration.PARAM_DEFAULT_MEDIA, null /* default*/); + final boolean showMapControls = GwtHttpRequestUtils.getBooleanParameter(RaceBoardViewConfiguration.PARAM_VIEW_SHOW_MAPCONTROLS, true /* default*/); + raceboardViewConfig = new RaceBoardViewConfiguration(activeCompetitorsFilterSetName, showLeaderboard, + showWindChart, showCompetitorsChart, showViewStreamlets, showViewStreamletColors, showViewSimulation, canReplayWhileLiveIsPossible, autoSelectMedia, defaultMedia); + + sailingService.getRaceboardData(regattaName, raceName, leaderboardName, leaderboardGroupName, eventId, new AsyncCallback() { + @Override + public void onSuccess(RaceboardDataDTO result) { + checkUrlParameters(result, canReplayWhileLiveIsPossible, showMapControls, + /* isScreenLargeEnoughToOfferChartSupport */ Document.get().getClientHeight() >= 600); + } + + @Override + public void onFailure(Throwable caught) { + reportError("Error trying to create the raceboard: " + caught.getMessage()); + } + }); + } + + 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 void checkUrlParameters(RaceboardDataDTO raceboardData, boolean canReplayWhileLiveIsPossible, boolean showMapControls, + boolean isScreenLargeEnoughToOfferChartSupport) { + if (!raceboardData.isValidLeaderboard()) { + createErrorPage(getStringMessages().noSuchLeaderboard()); + return; + } + if (eventId != null && !raceboardData.isValidEvent()) { + createErrorPage(getStringMessages().noSuchEvent()); + } + if (leaderboardGroupName != null) { + if(!raceboardData.isValidLeaderboardGroup()) { + createErrorPage(getStringMessages().leaderboardNotContainedInLeaderboardGroup(leaderboardName, leaderboardGroupName)); + return; + } + if (eventId != null && raceboardData.isValidLeaderboardGroup() && !raceboardData.isValidEvent()) { + createErrorPage(getStringMessages().leaderboardGroupNotContainedInEvent(leaderboardGroupName, eventId.toString())); + return; + } + } + if (raceboardData.getRace() == null) { + createErrorPage("Could not obtain a race with name " + raceName + " for a regatta with name " + regattaName); + return; + } + selectedRace = raceboardData.getRace(); + Window.setTitle(selectedRace.getName()); + RaceSelectionModel raceSelectionModel = new RaceSelectionModel(); + List singletonList = Collections.singletonList(selectedRace.getRaceIdentifier()); + raceSelectionModel.setSelection(singletonList); + Timer timer = new Timer(PlayModes.Replay, 1000l); + AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); + RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, asyncActionsExecutor, this, singletonList, 5000l /* requestInterval*/); + RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, getUserService(), asyncActionsExecutor, + raceboardData.getCompetitorAndTheirBoats(), timer, raceSelectionModel, leaderboardName, leaderboardGroupName, eventId, + raceboardViewConfig, RaceBoardEntryPoint.this, getStringMessages(), userAgent, raceTimesInfoProvider, showMapControls, + isScreenLargeEnoughToOfferChartSupport); + + createRaceBoardInOneScreenMode(raceBoardPanel, raceboardViewConfig); + } + + private FlowPanel createTimePanel(RaceBoardPanel raceBoardPanel) { + FlowPanel timeLineInnerBgPanel = new ResizableFlowPanel(); + timeLineInnerBgPanel.addStyleName("timeLineInnerBgPanel"); + timeLineInnerBgPanel.add(raceBoardPanel.getTimePanel()); + + FlowPanel timeLineInnerPanel = new ResizableFlowPanel(); + timeLineInnerPanel.add(timeLineInnerBgPanel); + timeLineInnerPanel.addStyleName("timeLineInnerPanel"); + + FlowPanel timelinePanel = new ResizableFlowPanel(); + timelinePanel.add(timeLineInnerPanel); + timelinePanel.addStyleName("timeLinePanel"); + + return timelinePanel; + } + + private void createRaceBoardInOneScreenMode(final RaceBoardPanel raceBoardPanel, + RaceBoardViewConfiguration raceboardViewConfiguration) { + final DockLayoutPanel p = new DockLayoutPanel(Unit.PX); + RootLayoutPanel.get().add(p); + final FlowPanel timePanel = createTimePanel(raceBoardPanel); + final Button toggleButton = raceBoardPanel.getTimePanel().getAdvancedToggleButton(); + toggleButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + boolean advancedModeShown = raceBoardPanel.getTimePanel().toggleAdvancedMode(); + if (advancedModeShown) { + p.setWidgetSize(timePanel, 96); + toggleButton.removeStyleDependentName("Closed"); + toggleButton.addStyleDependentName("Open"); + } else { + p.setWidgetSize(timePanel, 67); + toggleButton.addStyleDependentName("Closed"); + toggleButton.removeStyleDependentName("Open"); + } + } + }); + p.addSouth(timePanel, 67); + p.add(raceBoardPanel); + p.addStyleName("dockLayoutPanel"); + } +} From 6bf4db1a60b534f401588b870a8e86d681e9f890 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Mon, 23 Nov 2015 15:56:38 +0100 Subject: [PATCH 031/306] Fixed leaderboards splitpanel sizing to make sure it is not too wide on small screens/mobile devices --- .../sailing/gwt/ui/raceboard/SideBySideComponentViewer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java index b16b50a7bc5..6971e1eed1a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java @@ -130,7 +130,8 @@ public class SideBySideComponentViewer implements ComponentViewer, UserStatusEve this.mainPanel = new LayoutPanel() { @Override public void onResize() { - savedSplitPosition = Math.min(leftScrollPanel.getOffsetWidth(), Window.getClientWidth() - 40); + int leftWidth = leftScrollPanel.getOffsetWidth(); + savedSplitPosition = Math.min(leftWidth > 0 ? leftWidth : savedSplitPosition, Window.getClientWidth() - 40); splitLayoutPanel.setWidgetSize(leftScrollPanel, savedSplitPosition); super.onResize(); } From d2e22aa22c7c3d2b0c2903bf2d4eb6c5ace0306e Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Mon, 23 Nov 2015 15:58:31 +0100 Subject: [PATCH 032/306] Adjusted RaceBoard.css to avoid overlapping of headers on small screens/mobile devices --- java/com.sap.sailing.gwt.ui/RaceBoard.css | 34 +++++++++++++++++++ .../gwt/ui/raceboard/RaceBoardPanel.java | 3 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/RaceBoard.css b/java/com.sap.sailing.gwt.ui/RaceBoard.css index 0e7cd4f163c..066b94c4372 100644 --- a/java/com.sap.sailing.gwt.ui/RaceBoard.css +++ b/java/com.sap.sailing.gwt.ui/RaceBoard.css @@ -6,6 +6,13 @@ height: 0; clear: both; visibility: hidden; + + RegattaRaceInformation-Header + RaceName-Label + RaceSeriesAndFleet-Label + + RegattaAndRaceTime-Header??? + RegattaName-Anchor */ @@ -664,6 +671,33 @@ img.openColumn { margin-top: 5px; } +@media (max-width: 600px) { + .RegattaRaceInformation-Header { + left: 110px; + } + .RegattaRaceInformation-Header .RaceName-Label { + display: block; + font-size: 16px; + padding: 2px 0; + } + .RegattaRaceInformation-Header .RaceSeriesAndFleet-Label { + display: block; + margin-top: 5px; + } + .RegattaAndRaceTime-Header { + top: 11px; + } + .RegattaAndRaceTime-Header .RegattaName-Anchor { + width: 115px; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + white-space: nowrap; + padding: 2px 0 2px 20px; + background-position: 4px -1px; + } +} + .VideoPopup-Close-Button { border: none; } 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 b1820943443..8161b940faa 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 @@ -380,7 +380,7 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi if (fleetForRaceName.equals(LeaderboardNameConstants.DEFAULT_FLEET_NAME)) { fleetForRaceName = ""; } else { - fleetForRaceName = " - "+fleetForRaceName; + fleetForRaceName = (seriesName.isEmpty() ? "" : " - ") + fleetForRaceName; } final Label raceNameLabel = new Label(stringMessages.race() + " " + raceColumn.getRaceColumnName()); raceNameLabel.setStyleName("RaceName-Label"); @@ -390,6 +390,7 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi raceInformationHeader.add(raceNameLabel); raceInformationHeader.add(raceAdditionalInformationLabel); final Anchor regattaNameAnchor = new Anchor(raceIdentifier.getRegattaName()); + regattaNameAnchor.setTitle(raceIdentifier.getRegattaName()); if (eventId != null) { // we don't use the EntryPointLinkFactory here, because of lacking support for Places String debugParam = Window.Location.getParameter("gwt.codesvr"); From f6200c01a79d5480c6c4fba26b22efd2d1ba7e5f Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 24 Nov 2015 09:44:45 +0100 Subject: [PATCH 033/306] Adjusted RaceBoard header sizing to work on RaceMap width instead of window width --- java/com.sap.sailing.gwt.ui/RaceBoard.css | 48 +- .../gwt/ui/client/shared/racemap/RaceMap.java | 5207 +++++++++-------- 2 files changed, 2629 insertions(+), 2626 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/RaceBoard.css b/java/com.sap.sailing.gwt.ui/RaceBoard.css index 066b94c4372..b28780542c1 100644 --- a/java/com.sap.sailing.gwt.ui/RaceBoard.css +++ b/java/com.sap.sailing.gwt.ui/RaceBoard.css @@ -671,31 +671,29 @@ img.openColumn { margin-top: 5px; } -@media (max-width: 600px) { - .RegattaRaceInformation-Header { - left: 110px; - } - .RegattaRaceInformation-Header .RaceName-Label { - display: block; - font-size: 16px; - padding: 2px 0; - } - .RegattaRaceInformation-Header .RaceSeriesAndFleet-Label { - display: block; - margin-top: 5px; - } - .RegattaAndRaceTime-Header { - top: 11px; - } - .RegattaAndRaceTime-Header .RegattaName-Anchor { - width: 115px; - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; - white-space: nowrap; - padding: 2px 0 2px 20px; - background-position: 4px -1px; - } +.compactHeader .RegattaRaceInformation-Header { + left: 110px; +} +.compactHeader .RegattaRaceInformation-Header .RaceName-Label { + display: block; + font-size: 16px; + padding: 2px 0; +} +.compactHeader .RegattaRaceInformation-Header .RaceSeriesAndFleet-Label { + display: block; + margin-top: 5px; +} +.compactHeader .RegattaAndRaceTime-Header { + top: 11px; +} +.compactHeader .RegattaAndRaceTime-Header .RegattaName-Anchor { + width: 115px; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + white-space: nowrap; + padding: 2px 0 2px 20px; + background-position: 4px -1px; } .VideoPopup-Close-Button { 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 b4657f0d566..c2b5e540db8 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,2601 +1,2606 @@ -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.canvas.dom.client.CssColor; -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.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.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.Tack; -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.scalablevalue.impl.ScalableBearing; -import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; -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.NumberFormatterFactory; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -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.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.ControlPointDTO; -import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; -import com.sap.sailing.gwt.ui.shared.GPSFixDTO; -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.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.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.Component; -import com.sap.sse.gwt.client.shared.components.SettingsDialog; -import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; - -public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSelectionChangeListener, RaceSelectionChangeListener, - RaceTimesInfoProviderListener, TailFactory, Component, RequiresDataInitialization, RequiresResize, QuickRankProvider { - public static final String GET_RACE_MAP_DATA_CATEGORY = "getRaceMapData"; - public static final String GET_WIND_DATA_CATEGORY = "getWindData"; - - 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; - - /** - * 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 GPSFixDTO}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 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 List selectedRaces; - - /** - * 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 final RaceMapSettings settings; - - 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 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 showViewStreamlets; - private final boolean showViewStreamletColors; - private final boolean showViewSimulation; - - 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; - - private final boolean showMapControls; - - /** - * 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(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - ErrorReporter errorReporter, Timer timer, CompetitorSelectionProvider competitorSelection, StringMessages stringMessages, - boolean showMapControls, boolean showViewStreamlets, boolean showViewStreamletColors, boolean showViewSimulation, - RegattaAndRaceIdentifier raceIdentifier, CombinedWindPanelStyle combinedWindPanelStyle, boolean showHeaderPanel) { - this.setSize("100%", "100%"); - this.showMapControls = showMapControls; - this.stringMessages = stringMessages; - this.sailingService = sailingService; - this.raceIdentifier = raceIdentifier; - this.asyncActionsExecutor = asyncActionsExecutor; - this.errorReporter = errorReporter; - this.timer = timer; - timer.addTimeListener(this); - raceMapImageManager = new RaceMapImageManager(); - markDTOs = new HashMap(); - courseSidelines = new HashMap<>(); - courseMiddleLines = new HashMap<>(); - infoOverlaysForLinesForCourseGeometry = new HashMap<>(); - boatOverlays = new HashMap(); - competitorInfoOverlays = new HashMap(); - windSensorOverlays = new HashMap(); - courseMarkOverlays = new HashMap(); - this.competitorSelection = competitorSelection; - competitorSelection.addCompetitorSelectionChangeListener(this); - settings = new RaceMapSettings(); - coordinateSystem = new DelegateCoordinateSystem(new IdentityCoordinateSystem()); - fixesAndTails = new FixesAndTails(coordinateSystem); - updateCoordinateSystemFromSettings(); - lastTimeChangeBeforeInitialization = null; - isMapInitialized = false; - this.showViewStreamlets = showViewStreamlets; - this.showViewStreamletColors = showViewStreamletColors; - this.showViewSimulation = showViewSimulation; - this.hasPolar = false; - headerPanel = new FlowPanel(); - headerPanel.setStyleName("RaceMap-HeaderPanel"); - panelForLeftHeaderLabels = new AbsolutePanel(); - panelForRightHeaderLabels = new AbsolutePanel(); - initializeData(showMapControls, showHeaderPanel); - combinedWindPanel = new CombinedWindPanel(raceMapImageManager, combinedWindPanelStyle, stringMessages, coordinateSystem); - combinedWindPanel.setVisible(false); - trueNorthIndicatorPanel = new TrueNorthIndicatorPanel(this, raceMapImageManager, combinedWindPanelStyle, stringMessages, coordinateSystem); - trueNorthIndicatorPanel.setVisible(true); - orientationChangeInProgress = false; - } - - /** - * 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; - } - - private void updateCoordinateSystemFromSettings() { - 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(showMapControls, /* 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(showMapControls, /* 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(); - // 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; - } - } - }); - } - } - - 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); - RaceMap.this.add(map, 0, 0); - if (showHeaderPanel) { - Image sapLogo = createSAPLogo(); - RaceMap.this.add(sapLogo); - } - map.setControls(ControlPosition.LEFT_TOP, combinedWindPanel); - combinedWindPanel.getParent().addStyleName("CombinedWindPanelParentDiv"); - map.setControls(ControlPosition.LEFT_TOP, trueNorthIndicatorPanel); - trueNorthIndicatorPanel.getParent().addStyleName("TrueNorthIndicatorPanelParentDiv"); - - 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(); - settings.getZoomSettings().setTypesToConsiderOnZoom(emptyList); - } - // 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(); - settings.getZoomSettings().setTypesToConsiderOnZoom(emptyList); - } - }); - 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 (showViewStreamlets) { - streamletOverlay.setColors(showViewStreamletColors); - streamletOverlay.setVisible(true); - } - - if (showViewSimulation) { - // determine availability of polar diagram - setHasPolar(); - // initialize simulation canvas - simulationOverlay = new RaceSimulationOverlay(getMap(), /* zIndex */ 0, raceIdentifier, sailingService, stringMessages, asyncActionsExecutor, coordinateSystem); - simulationOverlay.addToMap(); - simulationOverlay.setVisible(false); - } - if (showHeaderPanel) { - createHeaderPanel(map); - } - 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"); - 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.setTitle(stringMessages.settings()); - settingsButton.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - new SettingsDialog(component, stringMessages).show(); - } - }); - map.setControls(ControlPosition.RIGHT_TOP, settingsButton); - } - - private void removeTransitions() { - // remove the canvas animations for boats - for (BoatOverlay 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(); - } - } - - public void redraw() { - timeChanged(timer.getTime(), null); - } - - Map getBoatOverlays() { - return Collections.unmodifiableMap(boatOverlays); - } - - public MapWidget getMap() { - return map; - } - - /** - * @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 onRaceSelectionChange(List selectedRaces) { - mapFirstZoomDone = false; - // TODO bug 494: reset zoom settings to user preferences - this.selectedRaces = selectedRaces; - } - - @Override - public void raceTimesInfosReceived(Map raceTimesInfos, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { - timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); - this.lastRaceTimesInfo = raceTimesInfos.get(selectedRaces.get(0)); - } - - /** - * 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 (selectedRaces != null && !selectedRaces.isEmpty()) { - RegattaAndRaceIdentifier race = selectedRaces.get(selectedRaces.size() - 1); - final Iterable competitorsToShow = getCompetitorsToShow(); - - if (race != null) { - final com.sap.sse.common.Util.Triple, Map, Map> fromAndToAndOverlap = - fixesAndTails.computeFromAndTo(newTime, competitorsToShow, settings.getEffectiveTailLengthInMilliseconds()); - int requestID = ++boatPositionRequestIDCounter; - // For those competitors for which the tails don't overlap (and therefore will be replaced by the new tail coming from the server) - // we expect some potential delay in computing the full tail. Therefore, in those cases we fire two requests: one fetching only the - // boat positions at newTime with zero tail length; and another one fetching everything else. - GetRaceMapDataAction getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping = getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping(fromAndToAndOverlap, race, newTime); - if (getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping != null) { - asyncActionsExecutor.execute(getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping, GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); - requestID = ++boatPositionRequestIDCounter; - } - // next, do the full thing; being the later call, if request throttling kicks in, the later call - // supersedes the earlier call which may get dropped then - GetRaceMapDataAction getRaceMapDataAction = new GetRaceMapDataAction(sailingService, competitorSelection.getAllCompetitors(), race, - useNullAsTimePoint() ? null : newTime, fromAndToAndOverlap.getA(), fromAndToAndOverlap.getB(), /* extrapolate */ true, (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null)); - asyncActionsExecutor.execute(getRaceMapDataAction, GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); - - // 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); - 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); - } - }); - } - } - } - } - - /** - * 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. Non-overlapping position requests typically occur for the - * first request when no fix at all is known for the competitor yet, and 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 the non-overlapping requests, this method creates a separate request which only loads boat positions, quick ranks, sidelines and - * mark positions for the zero-length interval at newTime, 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. - */ - private GetRaceMapDataAction getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping( - Triple, Map, Map> fromAndToAndOverlap, - RegattaAndRaceIdentifier race, Date newTime) { - Map fromTimes = new HashMap<>(); - Map toTimes = new HashMap<>(); - for (Map.Entry e : fromAndToAndOverlap.getC().entrySet()) { - if (!e.getValue()) { - // no overlap; add competitor to request - fromTimes.put(e.getKey(), newTime); - toTimes.put(e.getKey(), newTime); - } - } - final GetRaceMapDataAction result; - if (!fromTimes.isEmpty()) { - result = new GetRaceMapDataAction(sailingService, competitorSelection.getAllCompetitors(), - race, useNullAsTimePoint() ? null : newTime, fromTimes, toTimes, /* extrapolate */true, (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null)); - } else { - result = null; - } - return result; - } - - private AsyncCallback getRaceMapDataCallback( - final Date oldTime, - final Date newTime, - final Map hasTailOverlapForCompetitor, - final Iterable competitorsToShow, final int requestID) { - return 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) { - quickRanks = raceMapDataDTO.quickRanks; - competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber = - raceMapDataDTO.competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber; - if (showViewSimulation && settings.isShowSimulationOverlay()) { - lastLegNumber = raceMapDataDTO.coursePositions.currentLegNumber; - simulationOverlay.updateLeg(Math.max(lastLegNumber,1), /* clearCanvas */ false, raceMapDataDTO.simulationResultVersion); - } - // process response only if not received out of order - if (startedProcessingRequestID < requestID) { - startedProcessingRequestID = requestID; - // Do boat specific actions - Map> boatData = raceMapDataDTO.boatPositions; - long timeForPositionTransitionMillis = calculateTimeForPositionTransition(newTime, oldTime); - fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, timeForPositionTransitionMillis); - showBoatsOnMap(newTime, timeForPositionTransitionMillis, getCompetitorsToShow()); - showCompetitorInfoOnMap(newTime, timeForPositionTransitionMillis, competitorSelection.getSelectedFilteredCompetitors()); - if (douglasMarkers != null) { - removeAllMarkDouglasPeuckerpoints(); - } - if (maneuverMarkers != null) { - removeAllManeuverMarkers(); - } - - // Do mark specific actions - showCourseMarksOnMap(raceMapDataDTO.coursePositions); - if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { - updateCoordinateSystemFromSettings(); - } - showCourseSidelinesOnMap(raceMapDataDTO.courseSidelines); - showStartAndFinishAndCourseMiddleLines(raceMapDataDTO.coursePositions); - showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); - // 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); - - // 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; - } - } - }; - } - - 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) { - 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 = createCourseMarkOverlay(RaceMapOverlaysZIndexes.COURSEMARK_ZINDEX, markDTO); - courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); - courseMarkOverlay.setBuoyZoneRadiusInMeter(settings.getBuoyZoneRadiusInMeters()); - courseMarkOverlay.setSelected(isSelected); - courseMarkOverlays.put(markDTO.getName(), courseMarkOverlay); - markDTOs.put(markDTO.getName(), markDTO); - courseMarkOverlay.addToMap(); - } else { - courseMarkOverlay.setMarkPosition(markDTO.position); - courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); - courseMarkOverlay.setBuoyZoneRadiusInMeter(settings.getBuoyZoneRadiusInMeters()); - courseMarkOverlay.setSelected(isSelected); - 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)) { - GPSFixDTO 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.setPosition(lastBoatFix.position, timeForPositionTransitionMillis); - competitorInfoOverlay.addToMap(); - } else { - 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 calculateTimeForPositionTransition(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; - } - - private void showBoatsOnMap(final Date newTime, final long timeForPositionTransitionMillis, final Iterable competitorsToShow) { - if (map != null) { - Date tailsFromTime = new Date(newTime.getTime() - settings.getEffectiveTailLengthInMilliseconds()); - Date tailsToTime = newTime; - Set competitorDTOsOfUnusedTails = new HashSet(fixesAndTails.getCompetitorsWithTails()); - Set competitorDTOsOfUnusedBoatCanvases = new HashSet(boatOverlays.keySet()); - for (CompetitorDTO competitorDTO : competitorsToShow) { - if (fixesAndTails.hasFixesFor(competitorDTO)) { - Polyline tail = fixesAndTails.getTail(competitorDTO); - if (tail == null) { - tail = fixesAndTails.createTailAndUpdateIndices(competitorDTO, tailsFromTime, tailsToTime, this); - tail.setMap(map); - } else { - fixesAndTails.updateTail(tail, competitorDTO, tailsFromTime, tailsToTime, - (int) (timeForPositionTransitionMillis==-1?-1:timeForPositionTransitionMillis/2)); - competitorDTOsOfUnusedTails.remove(competitorDTO); - PolylineOptions newOptions = createTailStyle(competitorDTO, displayHighlighted(competitorDTO)); - tail.setOptions(newOptions); - } - boolean usedExistingBoatCanvas = updateBoatCanvasForCompetitor(competitorDTO, newTime, timeForPositionTransitionMillis); - if (usedExistingBoatCanvas) { - competitorDTOsOfUnusedBoatCanvases.remove(competitorDTO); - } - } - } - for (CompetitorDTO unusedBoatCanvasCompetitorDTO : competitorDTOsOfUnusedBoatCanvases) { - BoatOverlay 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, double distanceInKm) { - double distianceRad = distanceInKm / 6371.0; // r = 6371 means earth's radius in km - 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(distianceRad) + - Math.cos(lat1) * Math.sin(distianceRad) * Math.cos(bearingRad)); - double lon2 = lon1 + Math.atan2(Math.sin(bearingRad)*Math.sin(distianceRad)*Math.cos(lat1), - Math.cos(distianceRad)-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; - } - - private void showAdvantageLine(Iterable competitorsToShow, Date date) { - 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 - GPSFixDTO 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) { - double advantageLineLengthInKm = 1.0; // TODO this should probably rather scale with the visible - // area of the map; bug 616 - double distanceFromBoatPositionInKm = visibleLeaderInfo.getB().getBoatClass() - .getHullLengthInMeters() / 1000.; // 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 (legInfoDTO.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; - } - LatLng advantageLinePos1 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, - coordinateSystem.mapDegreeBearing(rotatedBearingDeg1), advantageLineLengthInKm / 2.0); - LatLng advantageLinePos2 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, - coordinateSystem.mapDegreeBearing(rotatedBearingDeg2), advantageLineLengthInKm / 2.0); - 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); - MVCArray pointsAsArray = MVCArray.newInstance(); - pointsAsArray.insertAt(0, advantageLinePos1); - pointsAsArray.insertAt(1, advantageLinePos2); - advantageLine.setPath(pointsAsArray); - - advantageLineMouseOverHandler = new AdvantageLineMouseOverMapHandler( - bearingOfCombinedWindInDeg, new Date(windFix.measureTimepoint)); - advantageLine.addMouseOverHandler(advantageLineMouseOverHandler); - advantageLine.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - advantageLine.setMap(map); - } else { - advantageLine.getPath().removeAt(1); - advantageLine.getPath().removeAt(0); - advantageLine.getPath().insertAt(0, advantageLinePos1); - advantageLine.getPath().insertAt(1, advantageLinePos2); - advantageLineMouseOverHandler.setTrueWindBearing(bearingOfCombinedWindInDeg); - advantageLineMouseOverHandler.setDate(new Date(windFix.measureTimepoint)); - } - drawAdvantageLine = true; - } - } - if (!drawAdvantageLine) { - if (advantageLine != null) { - advantageLine.setMap(null); - advantageLine = null; - } - } - } - } - - private final StringBuilder windwardStartLineMarkToFirstMarkLineText = new StringBuilder(); - private final StringBuilder leewardStartLineMarkToFirstMarkLineText = new StringBuilder(); - - private void showStartLineToFirstMarkTriangle(final CoursePositionsDTO courseDTO){ - final List startMarkPositions = courseDTO.getStartMarkPositions(); - 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.startLineAngleToCombinedWind != null) { - startLineAdvantageText.replace(0, startLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( - NumberFormat.getFormat("0.0").format(courseDTO.startLineLengthInMeters), - NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.startLineAngleToCombinedWind)), - 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.finishLineAngleToCombinedWind != null) { - finishLineAdvantageText.replace(0, finishLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( - NumberFormat.getFormat("0.0").format(courseDTO.finishLineLengthInMeters), - NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.finishLineAngleToCombinedWind)), - 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 (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.addMouseOverHandler(new MouseOverMapHandler() { - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(lineInfoProvider.getLineInfo()); - } - }); - lineToShowOrRemoveOrUpdate.addMouseOutMoveHandler(new MouseOutMapHandler() { - @Override - public void onEvent(MouseOutMapEvent event) { - map.setTitle(""); - } - }); - lineToShowOrRemoveOrUpdate.setMap(map); - - } 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; - } - - private 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 oldZoomSettings = 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()); - } - settings.getZoomSettings().setTypesToConsiderOnZoom(oldZoomSettings); - 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; - GPSFixDTO 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); - } - - protected CourseMarkOverlay createCourseMarkOverlay(int zIndex, final MarkDTO markDTO) { - final CourseMarkOverlay courseMarkOverlay = new CourseMarkOverlay(map, zIndex, markDTO, coordinateSystem); - courseMarkOverlay.addClickHandler(new ClickMapHandler() { - @Override - public void onEvent(ClickMapEvent event) { - LatLng latlng = courseMarkOverlay.getMarkLatLngPosition(); - showMarkInfoWindow(markDTO, latlng); - } - }); - return courseMarkOverlay; - } - - private CompetitorInfoOverlay createCompetitorInfoOverlay(int zIndex, final CompetitorDTO competitorDTO) { - String infoText = competitorDTO.getSailID() == null || competitorDTO.getSailID().isEmpty() ? competitorDTO.getName() : competitorDTO.getSailID(); - return new CompetitorInfoOverlay(map, zIndex, competitorSelection.getColor(competitorDTO, raceIdentifier), infoText, coordinateSystem); - } - - 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(); - } - GPSFixDTO 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(); - } - GPSFixDTO 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; - } - - 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, GPSFixDTO lastFix) { - final VerticalPanel vPanel = new VerticalPanel(); - vPanel.setWidth("350px"); - 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 (!selectedRaces.isEmpty()) { - RegattaAndRaceIdentifier race = selectedRaces.get(selectedRaces.size() - 1); - 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; - } - - private Iterable getCompetitorsToShow() { - Iterable result; - Iterable selection = competitorSelection.getSelectedCompetitors(); - if (!settings.isShowOnlySelectedCompetitors() || Util.isEmpty(selection)) { - result = competitorSelection.getFilteredCompetitors(); - } else { - result = selection; - } - 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 (GPSFixDTO 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)) { - LatLng latLng = coordinateSystem.toLatLng(maneuver.position); - Marker maneuverMarker = raceMapImageManager.maneuverIconsForTypeAndTargetTack.get(new com.sap.sse.common.Util.Pair(maneuver.type, maneuver.newTack)); - MarkerOptions options = MarkerOptions.newInstance(); - options.setTitle(maneuver.toString(stringMessages)); - options.setIcon(maneuverMarker.getIcon_MarkerImage()); - Marker marker = Marker.newInstance(options); - marker.setPosition(latLng); - 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 GPSFixDTO getBoatFix(CompetitorDTO competitorDTO, Date date) { - GPSFixDTO result = null; - List competitorFixes = fixesAndTails.getFixes(competitorDTO); - if (competitorFixes != null && !competitorFixes.isEmpty()) { - int i = Collections.binarySearch(competitorFixes, new GPSFixDTO(date, null, null, (WindDTO) null, null, null, false), - new Comparator() { - @Override - public int compare(GPSFixDTO o1, GPSFixDTO 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 GPSFixDTO fixBefore = competitorFixes.get(i-1); - final GPSFixDTO fixAfter = competitorFixes.get(i); - final GPSFixDTO 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 GPSFixDTO(date, betweenPosition, betweenSpeed, closer.degreesBoatToTheWind, - closer.tack, closer.legType, fixBefore.extrapolated || fixAfter.extrapolated); - } - } else { - // perfect match - final GPSFixDTO 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)) { - BoatOverlay 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 stringMessages.map(); - } - - @Override - public Widget getEntryWidget() { - return this; - } - - @Override - public boolean hasSettings() { - return true; - } - - @Override - public SettingsDialogComponent getSettingsDialogComponent() { - return new RaceMapSettingsDialogComponent(settings, stringMessages, this.showViewSimulation && this.hasPolar); - } - - @Override - public void updateSettings(RaceMapSettings newSettings) { - boolean maneuverTypeSelectionChanged = false; - boolean requiredRedraw = false; - for (ManeuverType maneuverType : ManeuverType.values()) { - if (newSettings.isShowManeuverType(maneuverType) != settings.isShowManeuverType(maneuverType)) { - maneuverTypeSelectionChanged = true; - settings.showManeuverType(maneuverType, newSettings.isShowManeuverType(maneuverType)); - } - } - 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()) { - settings.setShowDouglasPeuckerPoints(true); - removeAllMarkDouglasPeuckerpoints(); - showMarkDouglasPeuckerPoints(lastDouglasPeuckerResult); - } else if (!newSettings.isShowDouglasPeuckerPoints()) { - settings.setShowDouglasPeuckerPoints(false); - removeAllMarkDouglasPeuckerpoints(); - } - } - if (newSettings.getTailLengthInMilliseconds() != settings.getTailLengthInMilliseconds()) { - settings.setTailLengthInMilliseconds(newSettings.getTailLengthInMilliseconds()); - requiredRedraw = true; - } - if (newSettings.getBuoyZoneRadiusInMeters() != settings.getBuoyZoneRadiusInMeters()) { - settings.setBuoyZoneRadiusInMeters(newSettings.getBuoyZoneRadiusInMeters()); - requiredRedraw = true; - } - if (newSettings.isShowOnlySelectedCompetitors() != settings.isShowOnlySelectedCompetitors()) { - settings.setShowOnlySelectedCompetitors(newSettings.isShowOnlySelectedCompetitors()); - requiredRedraw = true; - } - if (newSettings.isShowSelectedCompetitorsInfo() != settings.isShowSelectedCompetitorsInfo()) { - settings.setShowSelectedCompetitorsInfo(newSettings.isShowSelectedCompetitorsInfo()); - requiredRedraw = true; - } - if (!newSettings.getZoomSettings().equals(settings.getZoomSettings())) { - settings.setZoomSettings(newSettings.getZoomSettings()); - if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { - removeTransitions(); - zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(this)); - } - } - if (!newSettings.getHelpLinesSettings().equals(settings.getHelpLinesSettings())) { - settings.setHelpLinesSettings(newSettings.getHelpLinesSettings()); - requiredRedraw = true; - } - if (newSettings.isShowWindStreamletOverlay() != settings.isShowWindStreamletOverlay()) { - settings.setShowWindStreamletOverlay(newSettings.isShowWindStreamletOverlay()); - streamletOverlay.setVisible(newSettings.isShowWindStreamletOverlay()); - } - if (newSettings.isShowWindStreamletColors() != settings.isShowWindStreamletColors()) { - settings.setShowWindStreamletColors(newSettings.isShowWindStreamletColors()); - streamletOverlay.setColors(newSettings.isShowWindStreamletColors()); - } - if (newSettings.isShowSimulationOverlay() != settings.isShowSimulationOverlay()) { - settings.setShowSimulationOverlay(newSettings.isShowSimulationOverlay()); - simulationOverlay.setVisible(newSettings.isShowSimulationOverlay()); - if (newSettings.isShowSimulationOverlay()) { - simulationOverlay.updateLeg(Math.max(lastLegNumber,1), true, -1 /* ensure ui-update */); - } - } - if (newSettings.isWindUp() != settings.isWindUp()) { - settings.setWindUp(newSettings.isWindUp()); - updateCoordinateSystemFromSettings(); - requiredRedraw = true; - } - if (requiredRedraw) { - redraw(); - } - } - - 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 { - GPSFixDTO 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)); - } - } - - @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) { - options.setStrokeColor(competitorSelection.getColor(competitor, raceIdentifier).getAsHtml()); - } else { - options.setStrokeColor(CssColor.make(200, 200, 200).toString()); - } - 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.addClickHandler(new ClickMapHandler() { - @Override - public void onEvent(ClickMapEvent event) { - showCompetitorInfoWindow(competitor, event.getMouseEvent().getLatLng()); - } - }); - result.addMouseOverHandler(new MouseOverMapHandler() { - @Override - public void onEvent(MouseOverMapEvent event) { - map.setTitle(competitor.getSailID() + ", " + competitor.getName()); - } - }); - result.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("http://www.sap.com", "_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 - // To play with the styles, check out http://gmaps-samples-v3.googlecode.com/svn/trunk/styledmaps/wizard/index.html. - // To convert an RGB color into the strange hue/saturation/lightness model used by the Google Map use - // http://software.stadtwerk.org/google_maps_colorizr/#water/all/123456/. - mapTypeStyles[3] = GoogleMapStyleHelper.createColorStyle(MapTypeStyleFeatureType.WATER, new RGBColor(0, 136, 255), 0, -70); - - 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; - } -} +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.canvas.dom.client.CssColor; +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.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.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.Tack; +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.scalablevalue.impl.ScalableBearing; +import com.sap.sailing.domain.common.scalablevalue.impl.ScalablePosition; +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.NumberFormatterFactory; +import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; +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.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.ControlPointDTO; +import com.sap.sailing.gwt.ui.shared.CoursePositionsDTO; +import com.sap.sailing.gwt.ui.shared.GPSFixDTO; +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.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.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.Component; +import com.sap.sse.gwt.client.shared.components.SettingsDialog; +import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; + +public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSelectionChangeListener, RaceSelectionChangeListener, + RaceTimesInfoProviderListener, TailFactory, Component, RequiresDataInitialization, RequiresResize, QuickRankProvider { + 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"; + + 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; + + /** + * 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 GPSFixDTO}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 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 List selectedRaces; + + /** + * 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 final RaceMapSettings settings; + + 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 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 showViewStreamlets; + private final boolean showViewStreamletColors; + private final boolean showViewSimulation; + + 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; + + private final boolean showMapControls; + + /** + * 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(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + ErrorReporter errorReporter, Timer timer, CompetitorSelectionProvider competitorSelection, StringMessages stringMessages, + boolean showMapControls, boolean showViewStreamlets, boolean showViewStreamletColors, boolean showViewSimulation, + RegattaAndRaceIdentifier raceIdentifier, CombinedWindPanelStyle combinedWindPanelStyle, boolean showHeaderPanel) { + this.setSize("100%", "100%"); + this.showMapControls = showMapControls; + this.stringMessages = stringMessages; + this.sailingService = sailingService; + this.raceIdentifier = raceIdentifier; + this.asyncActionsExecutor = asyncActionsExecutor; + this.errorReporter = errorReporter; + this.timer = timer; + timer.addTimeListener(this); + raceMapImageManager = new RaceMapImageManager(); + markDTOs = new HashMap(); + courseSidelines = new HashMap<>(); + courseMiddleLines = new HashMap<>(); + infoOverlaysForLinesForCourseGeometry = new HashMap<>(); + boatOverlays = new HashMap(); + competitorInfoOverlays = new HashMap(); + windSensorOverlays = new HashMap(); + courseMarkOverlays = new HashMap(); + this.competitorSelection = competitorSelection; + competitorSelection.addCompetitorSelectionChangeListener(this); + settings = new RaceMapSettings(); + coordinateSystem = new DelegateCoordinateSystem(new IdentityCoordinateSystem()); + fixesAndTails = new FixesAndTails(coordinateSystem); + updateCoordinateSystemFromSettings(); + lastTimeChangeBeforeInitialization = null; + isMapInitialized = false; + this.showViewStreamlets = showViewStreamlets; + this.showViewStreamletColors = showViewStreamletColors; + this.showViewSimulation = showViewSimulation; + this.hasPolar = false; + headerPanel = new FlowPanel(); + headerPanel.setStyleName("RaceMap-HeaderPanel"); + panelForLeftHeaderLabels = new AbsolutePanel(); + panelForRightHeaderLabels = new AbsolutePanel(); + initializeData(showMapControls, showHeaderPanel); + combinedWindPanel = new CombinedWindPanel(raceMapImageManager, combinedWindPanelStyle, stringMessages, coordinateSystem); + combinedWindPanel.setVisible(false); + trueNorthIndicatorPanel = new TrueNorthIndicatorPanel(this, raceMapImageManager, combinedWindPanelStyle, stringMessages, coordinateSystem); + trueNorthIndicatorPanel.setVisible(true); + orientationChangeInProgress = false; + } + + /** + * 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; + } + + private void updateCoordinateSystemFromSettings() { + 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(showMapControls, /* 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(showMapControls, /* 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(); + // 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; + } + } + }); + } + } + + 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); + RaceMap.this.add(map, 0, 0); + if (showHeaderPanel) { + Image sapLogo = createSAPLogo(); + RaceMap.this.add(sapLogo); + } + map.setControls(ControlPosition.LEFT_TOP, combinedWindPanel); + combinedWindPanel.getParent().addStyleName("CombinedWindPanelParentDiv"); + map.setControls(ControlPosition.LEFT_TOP, trueNorthIndicatorPanel); + trueNorthIndicatorPanel.getParent().addStyleName("TrueNorthIndicatorPanelParentDiv"); + + 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(); + settings.getZoomSettings().setTypesToConsiderOnZoom(emptyList); + } + // 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(); + settings.getZoomSettings().setTypesToConsiderOnZoom(emptyList); + } + }); + 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 (showViewStreamlets) { + streamletOverlay.setColors(showViewStreamletColors); + streamletOverlay.setVisible(true); + } + + if (showViewSimulation) { + // determine availability of polar diagram + setHasPolar(); + // initialize simulation canvas + simulationOverlay = new RaceSimulationOverlay(getMap(), /* zIndex */ 0, raceIdentifier, sailingService, stringMessages, asyncActionsExecutor, coordinateSystem); + simulationOverlay.addToMap(); + simulationOverlay.setVisible(false); + } + if (showHeaderPanel) { + createHeaderPanel(map); + } + 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"); + 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.setTitle(stringMessages.settings()); + settingsButton.addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + new SettingsDialog(component, stringMessages).show(); + } + }); + map.setControls(ControlPosition.RIGHT_TOP, settingsButton); + } + + private void removeTransitions() { + // remove the canvas animations for boats + for (BoatOverlay 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(); + } + } + + public void redraw() { + timeChanged(timer.getTime(), null); + } + + Map getBoatOverlays() { + return Collections.unmodifiableMap(boatOverlays); + } + + public MapWidget getMap() { + return map; + } + + /** + * @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 onRaceSelectionChange(List selectedRaces) { + mapFirstZoomDone = false; + // TODO bug 494: reset zoom settings to user preferences + this.selectedRaces = selectedRaces; + } + + @Override + public void raceTimesInfosReceived(Map raceTimesInfos, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { + timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); + this.lastRaceTimesInfo = raceTimesInfos.get(selectedRaces.get(0)); + } + + /** + * 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 (selectedRaces != null && !selectedRaces.isEmpty()) { + RegattaAndRaceIdentifier race = selectedRaces.get(selectedRaces.size() - 1); + final Iterable competitorsToShow = getCompetitorsToShow(); + + if (race != null) { + final com.sap.sse.common.Util.Triple, Map, Map> fromAndToAndOverlap = + fixesAndTails.computeFromAndTo(newTime, competitorsToShow, settings.getEffectiveTailLengthInMilliseconds()); + int requestID = ++boatPositionRequestIDCounter; + // For those competitors for which the tails don't overlap (and therefore will be replaced by the new tail coming from the server) + // we expect some potential delay in computing the full tail. Therefore, in those cases we fire two requests: one fetching only the + // boat positions at newTime with zero tail length; and another one fetching everything else. + GetRaceMapDataAction getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping = getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping(fromAndToAndOverlap, race, newTime); + if (getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping != null) { + asyncActionsExecutor.execute(getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping, GET_RACE_MAP_DATA_CATEGORY, + getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); + requestID = ++boatPositionRequestIDCounter; + } + // next, do the full thing; being the later call, if request throttling kicks in, the later call + // supersedes the earlier call which may get dropped then + GetRaceMapDataAction getRaceMapDataAction = new GetRaceMapDataAction(sailingService, competitorSelection.getAllCompetitors(), race, + useNullAsTimePoint() ? null : newTime, fromAndToAndOverlap.getA(), fromAndToAndOverlap.getB(), /* extrapolate */ true, (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null)); + asyncActionsExecutor.execute(getRaceMapDataAction, GET_RACE_MAP_DATA_CATEGORY, + getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); + + // 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); + 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); + } + }); + } + } + } + } + + /** + * 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. Non-overlapping position requests typically occur for the + * first request when no fix at all is known for the competitor yet, and 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 the non-overlapping requests, this method creates a separate request which only loads boat positions, quick ranks, sidelines and + * mark positions for the zero-length interval at newTime, 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. + */ + private GetRaceMapDataAction getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping( + Triple, Map, Map> fromAndToAndOverlap, + RegattaAndRaceIdentifier race, Date newTime) { + Map fromTimes = new HashMap<>(); + Map toTimes = new HashMap<>(); + for (Map.Entry e : fromAndToAndOverlap.getC().entrySet()) { + if (!e.getValue()) { + // no overlap; add competitor to request + fromTimes.put(e.getKey(), newTime); + toTimes.put(e.getKey(), newTime); + } + } + final GetRaceMapDataAction result; + if (!fromTimes.isEmpty()) { + result = new GetRaceMapDataAction(sailingService, competitorSelection.getAllCompetitors(), + race, useNullAsTimePoint() ? null : newTime, fromTimes, toTimes, /* extrapolate */true, (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null)); + } else { + result = null; + } + return result; + } + + private AsyncCallback getRaceMapDataCallback( + final Date oldTime, + final Date newTime, + final Map hasTailOverlapForCompetitor, + final Iterable competitorsToShow, final int requestID) { + return 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) { + quickRanks = raceMapDataDTO.quickRanks; + competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber = + raceMapDataDTO.competitorsInOrderOfWindwardDistanceTraveledWithOneBasedLegNumber; + if (showViewSimulation && settings.isShowSimulationOverlay()) { + lastLegNumber = raceMapDataDTO.coursePositions.currentLegNumber; + simulationOverlay.updateLeg(Math.max(lastLegNumber,1), /* clearCanvas */ false, raceMapDataDTO.simulationResultVersion); + } + // process response only if not received out of order + if (startedProcessingRequestID < requestID) { + startedProcessingRequestID = requestID; + // Do boat specific actions + Map> boatData = raceMapDataDTO.boatPositions; + long timeForPositionTransitionMillis = calculateTimeForPositionTransition(newTime, oldTime); + fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, timeForPositionTransitionMillis); + showBoatsOnMap(newTime, timeForPositionTransitionMillis, getCompetitorsToShow()); + showCompetitorInfoOnMap(newTime, timeForPositionTransitionMillis, competitorSelection.getSelectedFilteredCompetitors()); + if (douglasMarkers != null) { + removeAllMarkDouglasPeuckerpoints(); + } + if (maneuverMarkers != null) { + removeAllManeuverMarkers(); + } + + // Do mark specific actions + showCourseMarksOnMap(raceMapDataDTO.coursePositions); + if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { + updateCoordinateSystemFromSettings(); + } + showCourseSidelinesOnMap(raceMapDataDTO.courseSidelines); + showStartAndFinishAndCourseMiddleLines(raceMapDataDTO.coursePositions); + showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); + // 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); + + // 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; + } + } + }; + } + + 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) { + 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 = createCourseMarkOverlay(RaceMapOverlaysZIndexes.COURSEMARK_ZINDEX, markDTO); + courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); + courseMarkOverlay.setBuoyZoneRadiusInMeter(settings.getBuoyZoneRadiusInMeters()); + courseMarkOverlay.setSelected(isSelected); + courseMarkOverlays.put(markDTO.getName(), courseMarkOverlay); + markDTOs.put(markDTO.getName(), markDTO); + courseMarkOverlay.addToMap(); + } else { + courseMarkOverlay.setMarkPosition(markDTO.position); + courseMarkOverlay.setShowBuoyZone(settings.getHelpLinesSettings().isVisible(HelpLineTypes.BUOYZONE)); + courseMarkOverlay.setBuoyZoneRadiusInMeter(settings.getBuoyZoneRadiusInMeters()); + courseMarkOverlay.setSelected(isSelected); + 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)) { + GPSFixDTO 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.setPosition(lastBoatFix.position, timeForPositionTransitionMillis); + competitorInfoOverlay.addToMap(); + } else { + 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 calculateTimeForPositionTransition(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; + } + + private void showBoatsOnMap(final Date newTime, final long timeForPositionTransitionMillis, final Iterable competitorsToShow) { + if (map != null) { + Date tailsFromTime = new Date(newTime.getTime() - settings.getEffectiveTailLengthInMilliseconds()); + Date tailsToTime = newTime; + Set competitorDTOsOfUnusedTails = new HashSet(fixesAndTails.getCompetitorsWithTails()); + Set competitorDTOsOfUnusedBoatCanvases = new HashSet(boatOverlays.keySet()); + for (CompetitorDTO competitorDTO : competitorsToShow) { + if (fixesAndTails.hasFixesFor(competitorDTO)) { + Polyline tail = fixesAndTails.getTail(competitorDTO); + if (tail == null) { + tail = fixesAndTails.createTailAndUpdateIndices(competitorDTO, tailsFromTime, tailsToTime, this); + tail.setMap(map); + } else { + fixesAndTails.updateTail(tail, competitorDTO, tailsFromTime, tailsToTime, + (int) (timeForPositionTransitionMillis==-1?-1:timeForPositionTransitionMillis/2)); + competitorDTOsOfUnusedTails.remove(competitorDTO); + PolylineOptions newOptions = createTailStyle(competitorDTO, displayHighlighted(competitorDTO)); + tail.setOptions(newOptions); + } + boolean usedExistingBoatCanvas = updateBoatCanvasForCompetitor(competitorDTO, newTime, timeForPositionTransitionMillis); + if (usedExistingBoatCanvas) { + competitorDTOsOfUnusedBoatCanvases.remove(competitorDTO); + } + } + } + for (CompetitorDTO unusedBoatCanvasCompetitorDTO : competitorDTOsOfUnusedBoatCanvases) { + BoatOverlay 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, double distanceInKm) { + double distianceRad = distanceInKm / 6371.0; // r = 6371 means earth's radius in km + 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(distianceRad) + + Math.cos(lat1) * Math.sin(distianceRad) * Math.cos(bearingRad)); + double lon2 = lon1 + Math.atan2(Math.sin(bearingRad)*Math.sin(distianceRad)*Math.cos(lat1), + Math.cos(distianceRad)-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; + } + + private void showAdvantageLine(Iterable competitorsToShow, Date date) { + 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 + GPSFixDTO 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) { + double advantageLineLengthInKm = 1.0; // TODO this should probably rather scale with the visible + // area of the map; bug 616 + double distanceFromBoatPositionInKm = visibleLeaderInfo.getB().getBoatClass() + .getHullLengthInMeters() / 1000.; // 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 (legInfoDTO.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; + } + LatLng advantageLinePos1 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, + coordinateSystem.mapDegreeBearing(rotatedBearingDeg1), advantageLineLengthInKm / 2.0); + LatLng advantageLinePos2 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, + coordinateSystem.mapDegreeBearing(rotatedBearingDeg2), advantageLineLengthInKm / 2.0); + 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); + MVCArray pointsAsArray = MVCArray.newInstance(); + pointsAsArray.insertAt(0, advantageLinePos1); + pointsAsArray.insertAt(1, advantageLinePos2); + advantageLine.setPath(pointsAsArray); + + advantageLineMouseOverHandler = new AdvantageLineMouseOverMapHandler( + bearingOfCombinedWindInDeg, new Date(windFix.measureTimepoint)); + advantageLine.addMouseOverHandler(advantageLineMouseOverHandler); + advantageLine.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + advantageLine.setMap(map); + } else { + advantageLine.getPath().removeAt(1); + advantageLine.getPath().removeAt(0); + advantageLine.getPath().insertAt(0, advantageLinePos1); + advantageLine.getPath().insertAt(1, advantageLinePos2); + advantageLineMouseOverHandler.setTrueWindBearing(bearingOfCombinedWindInDeg); + advantageLineMouseOverHandler.setDate(new Date(windFix.measureTimepoint)); + } + drawAdvantageLine = true; + } + } + if (!drawAdvantageLine) { + if (advantageLine != null) { + advantageLine.setMap(null); + advantageLine = null; + } + } + } + } + + private final StringBuilder windwardStartLineMarkToFirstMarkLineText = new StringBuilder(); + private final StringBuilder leewardStartLineMarkToFirstMarkLineText = new StringBuilder(); + + private void showStartLineToFirstMarkTriangle(final CoursePositionsDTO courseDTO){ + final List startMarkPositions = courseDTO.getStartMarkPositions(); + 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.startLineAngleToCombinedWind != null) { + startLineAdvantageText.replace(0, startLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( + NumberFormat.getFormat("0.0").format(courseDTO.startLineLengthInMeters), + NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.startLineAngleToCombinedWind)), + 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.finishLineAngleToCombinedWind != null) { + finishLineAdvantageText.replace(0, finishLineAdvantageText.length(), " "+stringMessages.lineAngleToWindAndAdvantage( + NumberFormat.getFormat("0.0").format(courseDTO.finishLineLengthInMeters), + NumberFormat.getFormat("0.0").format(Math.abs(courseDTO.finishLineAngleToCombinedWind)), + 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 (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.addMouseOverHandler(new MouseOverMapHandler() { + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(lineInfoProvider.getLineInfo()); + } + }); + lineToShowOrRemoveOrUpdate.addMouseOutMoveHandler(new MouseOutMapHandler() { + @Override + public void onEvent(MouseOutMapEvent event) { + map.setTitle(""); + } + }); + lineToShowOrRemoveOrUpdate.setMap(map); + + } 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; + } + + private 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 oldZoomSettings = 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()); + } + settings.getZoomSettings().setTypesToConsiderOnZoom(oldZoomSettings); + 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; + GPSFixDTO 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); + } + + protected CourseMarkOverlay createCourseMarkOverlay(int zIndex, final MarkDTO markDTO) { + final CourseMarkOverlay courseMarkOverlay = new CourseMarkOverlay(map, zIndex, markDTO, coordinateSystem); + courseMarkOverlay.addClickHandler(new ClickMapHandler() { + @Override + public void onEvent(ClickMapEvent event) { + LatLng latlng = courseMarkOverlay.getMarkLatLngPosition(); + showMarkInfoWindow(markDTO, latlng); + } + }); + return courseMarkOverlay; + } + + private CompetitorInfoOverlay createCompetitorInfoOverlay(int zIndex, final CompetitorDTO competitorDTO) { + String infoText = competitorDTO.getSailID() == null || competitorDTO.getSailID().isEmpty() ? competitorDTO.getName() : competitorDTO.getSailID(); + return new CompetitorInfoOverlay(map, zIndex, competitorSelection.getColor(competitorDTO, raceIdentifier), infoText, coordinateSystem); + } + + 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(); + } + GPSFixDTO 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(); + } + GPSFixDTO 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; + } + + 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, GPSFixDTO lastFix) { + final VerticalPanel vPanel = new VerticalPanel(); + vPanel.setWidth("350px"); + 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 (!selectedRaces.isEmpty()) { + RegattaAndRaceIdentifier race = selectedRaces.get(selectedRaces.size() - 1); + 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; + } + + private Iterable getCompetitorsToShow() { + Iterable result; + Iterable selection = competitorSelection.getSelectedCompetitors(); + if (!settings.isShowOnlySelectedCompetitors() || Util.isEmpty(selection)) { + result = competitorSelection.getFilteredCompetitors(); + } else { + result = selection; + } + 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 (GPSFixDTO 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)) { + LatLng latLng = coordinateSystem.toLatLng(maneuver.position); + Marker maneuverMarker = raceMapImageManager.maneuverIconsForTypeAndTargetTack.get(new com.sap.sse.common.Util.Pair(maneuver.type, maneuver.newTack)); + MarkerOptions options = MarkerOptions.newInstance(); + options.setTitle(maneuver.toString(stringMessages)); + options.setIcon(maneuverMarker.getIcon_MarkerImage()); + Marker marker = Marker.newInstance(options); + marker.setPosition(latLng); + 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 GPSFixDTO getBoatFix(CompetitorDTO competitorDTO, Date date) { + GPSFixDTO result = null; + List competitorFixes = fixesAndTails.getFixes(competitorDTO); + if (competitorFixes != null && !competitorFixes.isEmpty()) { + int i = Collections.binarySearch(competitorFixes, new GPSFixDTO(date, null, null, (WindDTO) null, null, null, false), + new Comparator() { + @Override + public int compare(GPSFixDTO o1, GPSFixDTO 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 GPSFixDTO fixBefore = competitorFixes.get(i-1); + final GPSFixDTO fixAfter = competitorFixes.get(i); + final GPSFixDTO 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 GPSFixDTO(date, betweenPosition, betweenSpeed, closer.degreesBoatToTheWind, + closer.tack, closer.legType, fixBefore.extrapolated || fixAfter.extrapolated); + } + } else { + // perfect match + final GPSFixDTO 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)) { + BoatOverlay 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 stringMessages.map(); + } + + @Override + public Widget getEntryWidget() { + return this; + } + + @Override + public boolean hasSettings() { + return true; + } + + @Override + public SettingsDialogComponent getSettingsDialogComponent() { + return new RaceMapSettingsDialogComponent(settings, stringMessages, this.showViewSimulation && this.hasPolar); + } + + @Override + public void updateSettings(RaceMapSettings newSettings) { + boolean maneuverTypeSelectionChanged = false; + boolean requiredRedraw = false; + for (ManeuverType maneuverType : ManeuverType.values()) { + if (newSettings.isShowManeuverType(maneuverType) != settings.isShowManeuverType(maneuverType)) { + maneuverTypeSelectionChanged = true; + settings.showManeuverType(maneuverType, newSettings.isShowManeuverType(maneuverType)); + } + } + 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()) { + settings.setShowDouglasPeuckerPoints(true); + removeAllMarkDouglasPeuckerpoints(); + showMarkDouglasPeuckerPoints(lastDouglasPeuckerResult); + } else if (!newSettings.isShowDouglasPeuckerPoints()) { + settings.setShowDouglasPeuckerPoints(false); + removeAllMarkDouglasPeuckerpoints(); + } + } + if (newSettings.getTailLengthInMilliseconds() != settings.getTailLengthInMilliseconds()) { + settings.setTailLengthInMilliseconds(newSettings.getTailLengthInMilliseconds()); + requiredRedraw = true; + } + if (newSettings.getBuoyZoneRadiusInMeters() != settings.getBuoyZoneRadiusInMeters()) { + settings.setBuoyZoneRadiusInMeters(newSettings.getBuoyZoneRadiusInMeters()); + requiredRedraw = true; + } + if (newSettings.isShowOnlySelectedCompetitors() != settings.isShowOnlySelectedCompetitors()) { + settings.setShowOnlySelectedCompetitors(newSettings.isShowOnlySelectedCompetitors()); + requiredRedraw = true; + } + if (newSettings.isShowSelectedCompetitorsInfo() != settings.isShowSelectedCompetitorsInfo()) { + settings.setShowSelectedCompetitorsInfo(newSettings.isShowSelectedCompetitorsInfo()); + requiredRedraw = true; + } + if (!newSettings.getZoomSettings().equals(settings.getZoomSettings())) { + settings.setZoomSettings(newSettings.getZoomSettings()); + if (!settings.getZoomSettings().containsZoomType(ZoomTypes.NONE)) { + removeTransitions(); + zoomMapToNewBounds(settings.getZoomSettings().getNewBounds(this)); + } + } + if (!newSettings.getHelpLinesSettings().equals(settings.getHelpLinesSettings())) { + settings.setHelpLinesSettings(newSettings.getHelpLinesSettings()); + requiredRedraw = true; + } + if (newSettings.isShowWindStreamletOverlay() != settings.isShowWindStreamletOverlay()) { + settings.setShowWindStreamletOverlay(newSettings.isShowWindStreamletOverlay()); + streamletOverlay.setVisible(newSettings.isShowWindStreamletOverlay()); + } + if (newSettings.isShowWindStreamletColors() != settings.isShowWindStreamletColors()) { + settings.setShowWindStreamletColors(newSettings.isShowWindStreamletColors()); + streamletOverlay.setColors(newSettings.isShowWindStreamletColors()); + } + if (newSettings.isShowSimulationOverlay() != settings.isShowSimulationOverlay()) { + settings.setShowSimulationOverlay(newSettings.isShowSimulationOverlay()); + simulationOverlay.setVisible(newSettings.isShowSimulationOverlay()); + if (newSettings.isShowSimulationOverlay()) { + simulationOverlay.updateLeg(Math.max(lastLegNumber,1), true, -1 /* ensure ui-update */); + } + } + if (newSettings.isWindUp() != settings.isWindUp()) { + settings.setWindUp(newSettings.isWindUp()); + updateCoordinateSystemFromSettings(); + requiredRedraw = true; + } + if (requiredRedraw) { + redraw(); + } + } + + 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 { + GPSFixDTO 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)); + } + boolean isCompactHeader = this.getOffsetWidth() <= 600; + getLeftHeaderPanel().setStyleName(COMPACT_HEADER_STYLE, isCompactHeader); + getRightHeaderPanel().setStyleName(COMPACT_HEADER_STYLE, isCompactHeader); + } + + @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) { + options.setStrokeColor(competitorSelection.getColor(competitor, raceIdentifier).getAsHtml()); + } else { + options.setStrokeColor(CssColor.make(200, 200, 200).toString()); + } + 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.addClickHandler(new ClickMapHandler() { + @Override + public void onEvent(ClickMapEvent event) { + showCompetitorInfoWindow(competitor, event.getMouseEvent().getLatLng()); + } + }); + result.addMouseOverHandler(new MouseOverMapHandler() { + @Override + public void onEvent(MouseOverMapEvent event) { + map.setTitle(competitor.getSailID() + ", " + competitor.getName()); + } + }); + result.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("http://www.sap.com", "_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 + // To play with the styles, check out http://gmaps-samples-v3.googlecode.com/svn/trunk/styledmaps/wizard/index.html. + // To convert an RGB color into the strange hue/saturation/lightness model used by the Google Map use + // http://software.stadtwerk.org/google_maps_colorizr/#water/all/123456/. + mapTypeStyles[3] = GoogleMapStyleHelper.createColorStyle(MapTypeStyleFeatureType.WATER, new RGBColor(0, 136, 255), 0, -70); + + 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; + } +} From 4a863866fd0e8f0af425a817540a7b6e69875405 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 24 Nov 2015 12:09:39 +0100 Subject: [PATCH 034/306] Added documentation to explain resizing an feature toogle logic --- .../sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java | 6 +++++- .../com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java | 4 +++- .../sailing/gwt/ui/raceboard/SideBySideComponentViewer.java | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) 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 11658613ec0..488813e295a 100755 --- 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 @@ -98,8 +98,12 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { sailingService.getRaceboardData(regattaName, raceName, leaderboardName, leaderboardGroupName, eventId, new AsyncCallback() { @Override public void onSuccess(RaceboardDataDTO result) { + // Determine if the screen is large enough to display charts such as the competitor chart or the wind chart. + // This decision is made once based on the initial screen height. Resizing the window afterwards will have + // no impact on the chart support, i.e. they are available/unavailable based on the initial decision. + boolean isScreenLargeEnoughToOfferChartSupport = Document.get().getClientHeight() >= 600; checkUrlParameters(result, canReplayWhileLiveIsPossible, showMapControls, - /* isScreenLargeEnoughToOfferChartSupport */ Document.get().getClientHeight() >= 600); + isScreenLargeEnoughToOfferChartSupport); } @Override 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 8161b940faa..f54680eb93f 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 @@ -129,7 +129,7 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi * @param isScreenLargeEnoughToOfferChartSupport * if the screen is large enough to display charts such as the competitor chart or the wind chart, a * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and - * the charts are created. + * the charts are created. This decision is made once on startup in the {@link RaceBoardEntryPoint} class. */ public RaceBoardPanel(SailingServiceAsync sailingService, MediaServiceAsync mediaService, UserService userService, AsyncActionsExecutor asyncActionsExecutor, Map competitorsAndTheirBoats, @@ -179,6 +179,8 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi raceMap.getLeftHeaderPanel().add(raceInformationHeader); raceMap.getRightHeaderPanel().add(regattaAndRaceTimeInformationHeader); + // Determine if the screen is large enough to initially display the leaderboard panel on the left side of the + // map based on the initial screen width. Afterwards, the leaderboard panel visibility can be toggled as usual. boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard = Document.get().getClientWidth() >= 1024; leaderboardPanel = createLeaderboardPanel(leaderboardName, leaderboardGroupName, competitorSearchTextBox, isScreenLargeEnoughToInitiallyDisplayLeaderboard); leaderboardPanel.setTitle(stringMessages.leaderboard()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java index 6971e1eed1a..acab617a507 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java @@ -45,7 +45,7 @@ import com.sap.sse.security.ui.shared.UserDTO; public class SideBySideComponentViewer implements ComponentViewer, UserStatusEventHandler { private static final int DEFAULT_SOUTH_SPLIT_PANEL_HEIGHT = 200; - private final int MIN_LEADERBOARD_WIDTH = Math.min(432, Window.getClientWidth() - 40); // works well for 505 and ESS + private final int MIN_LEADERBOARD_WIDTH = Math.min(432, Window.getClientWidth() - 40); // fallback value "432" works well for 505 and ESS /** * Absolute Panel that informs its children about a resize @@ -131,6 +131,9 @@ public class SideBySideComponentViewer implements ComponentViewer, UserStatusEve @Override public void onResize() { int leftWidth = leftScrollPanel.getOffsetWidth(); + // The left scroll panel is potentially resized to ensure it is not too wide when the screen gets narrower, + // e.g. when resizing browser window or changing mobile device orientation. An offset of 40px is used, so + // the panels size slider and its toggle button is always accessable if it is open. savedSplitPosition = Math.min(leftWidth > 0 ? leftWidth : savedSplitPosition, Window.getClientWidth() - 40); splitLayoutPanel.setWidgetSize(leftScrollPanel, savedSplitPosition); super.onResize(); From eb379340dc3361f32819cb506415729cf88941bd Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 25 Nov 2015 12:18:18 +0100 Subject: [PATCH 035/306] Fixed SplitLayoutPanel EastToggleButton positioning, which was dependent on the label text length --- .../com/sap/sse/gwt/resources/css/CommonControls.css | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css index a15efd6a2c7..a30d6ccc9a7 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css @@ -169,8 +169,7 @@ body ::-webkit-scrollbar-track { .gwt-SplitLayoutPanel-EastToggleButton-Panel { position: absolute; - bottom: 77px; - margin-left: -35px; + bottom: -10px; } .gwt-SplitLayoutPanel-EastToggleButton { @@ -181,9 +180,14 @@ body ::-webkit-scrollbar-track { -moz-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); + transform: rotate(-90deg); + -webkit-transform-origin: 0 0; + -moz-transform-origin: 0 0; + -o-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; padding-right: 25px; padding-left: 30px; - margin-left: -15px; border-bottom: 1px solid #cecece; border-right: 1px solid #cecece; border-left: 1px solid #cecece; From 1bbcdca33d9168714f2c4434dc10a35002408460 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 25 Nov 2015 13:33:01 +0100 Subject: [PATCH 036/306] Fixed SplitLayoutPanel EastToggleButton background images positioning, which was dependent on the label text length --- java/com.sap.sailing.gwt.ui/RaceBoard.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/RaceBoard.css b/java/com.sap.sailing.gwt.ui/RaceBoard.css index b28780542c1..4b9a54e6bf4 100644 --- a/java/com.sap.sailing.gwt.ui/RaceBoard.css +++ b/java/com.sap.sailing.gwt.ui/RaceBoard.css @@ -580,7 +580,8 @@ img.openColumn { url(images/viewicons/@1x/img/SAP_RV_Leaderboard_GHOSTED.png), url(images/arrows_top_black_small.png); background-repeat: no-repeat, no-repeat; - background-position: 4% 50%, 96% 50% + background-position: 4% 50%, 96% 50%; + background-position: 5px center, calc(100% - 6px) center; } .gwt-SplitLayoutPanel-EastToggleButton-Closed-leaderboard { @@ -589,7 +590,8 @@ img.openColumn { url(images/viewicons/@1x/img/SAP_RV_Leaderboard_INACTIVE.png), url(images/arrows_bottom_black_small.png); background-repeat: no-repeat, no-repeat; - background-position: 4% 50%, 96% 50% + background-position: 4% 50%, 96% 50%; + background-position: 5px center, calc(100% - 6px) center; } .gwt-SplitLayoutPanel-NorthSouthToggleButton-Closed-media, From 9434c48e8c17d90b014c221e9d6c1b35fc0cccb4 Mon Sep 17 00:00:00 2001 From: fmittag Date: Wed, 25 Nov 2015 14:49:26 +0100 Subject: [PATCH 037/306] Added the leaderboard busyindicator to the info panel in the new design --- .../old/leaderboard/OldLeaderboard.java | 2 ++ .../old/leaderboard/OldLeaderboard.ui.xml | 5 +++- .../multileaderboard/OldMultiLeaderboard.java | 23 ++++++++++++++----- .../OldMultiLeaderboard.ui.xml | 15 +++++++----- .../ui/leaderboard/MultiLeaderboardPanel.java | 4 ++++ 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java index cc8fecd8c30..d7710ed954b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java @@ -52,6 +52,7 @@ public class OldLeaderboard extends Composite { @UiField DivElement lastScoringCommentDiv; @UiField DivElement scoringSchemeDiv; @UiField DivElement hasLiveRaceDiv; + @UiField DivElement busyIndicatorDiv; @UiField EventRegattaLeaderboardResources local_res; private LeaderboardPanel leaderboardPanel; @@ -166,6 +167,7 @@ public class OldLeaderboard extends Composite { this.autoRefreshTimer = timer; this.leaderboardPanel = leaderboardPanel; oldLeaderboardPanel.add(leaderboardPanel); + busyIndicatorDiv.appendChild(leaderboardPanel.getBusyIndicator().getElement()); } public void updatedLeaderboard(LeaderboardDTO leaderboard) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml index 593f26846a6..17696bafc13 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml @@ -5,11 +5,14 @@

    -
    +
    +
    +
     
    +
    diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java index 0d822d22293..c5319b1ea92 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java @@ -3,7 +3,8 @@ package com.sap.sailing.gwt.home.desktop.partials.old.multileaderboard; import java.util.Date; import com.google.gwt.core.client.GWT; -import com.google.gwt.dom.client.ParagraphElement; +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Style.Overflow; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; @@ -46,16 +47,19 @@ public class OldMultiLeaderboard extends Composite { @UiField Anchor settingsAnchor; @UiField Anchor autoRefreshAnchor; @UiField Anchor fullscreenAnchor; - @UiField ParagraphElement lastScoringUpdateTimeDiv; - @UiField ParagraphElement lastScoringUpdateTextDiv; - @UiField ParagraphElement lastScoringCommentDiv; - @UiField ParagraphElement scoringSchemeDiv; + @UiField DivElement lastScoringUpdateTimeDiv; + @UiField DivElement lastScoringUpdateTextDiv; + @UiField DivElement lastScoringCommentDiv; + @UiField DivElement scoringSchemeDiv; + @UiField DivElement busyIndicatorDiv; @UiField EventRegattaLeaderboardResources local_res; private MultiLeaderboardPanel multiLeaderboardPanel; private Timer autoRefreshTimer; private final OldMultiLeaderboardDelegate delegate; - + + private final DivElement emptyDivElement; + public OldMultiLeaderboard() { this(null); } @@ -69,6 +73,8 @@ public class OldMultiLeaderboard extends Composite { fullscreenAnchor.setTitle(StringMessages.INSTANCE.openFullscreenView()); this.delegate = delegate; this.setupFullscreenDelegate(); + emptyDivElement = Document.get().createDivElement(); + emptyDivElement.setInnerHTML(" "); } private void setupFullscreenDelegate() { @@ -168,6 +174,11 @@ public class OldMultiLeaderboard extends Composite { } public void updatedMultiLeaderboard(LeaderboardDTO leaderboard, boolean hasLiveRace) { + if(multiLeaderboardPanel.getSelectedLeaderboardPanel() != null) { + busyIndicatorDiv.removeAllChildren(); + busyIndicatorDiv.appendChild(multiLeaderboardPanel.getSelectedLeaderboardPanel().getBusyIndicator().getElement()); + busyIndicatorDiv.appendChild(emptyDivElement); + } if(leaderboard != null) { String comment = leaderboard.getComment() != null ? leaderboard.getComment() : ""; String scoringScheme = leaderboard.scoringScheme != null ? ScoringSchemeTypeFormatter.getDescription(leaderboard.scoringScheme, StringMessages.INSTANCE) : ""; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml index 737bef4a01d..d29f91043ab 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml @@ -6,18 +6,21 @@
    -

    -

    +
    +
    -
    +
    +
     
    +
    +
    -

    -

    +
    +
    -
    +
    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 96667e0ec23..6030ff94216 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 @@ -177,6 +177,10 @@ public class MultiLeaderboardPanel extends AbstractLazyComponent Date: Wed, 25 Nov 2015 15:29:18 +0100 Subject: [PATCH 038/306] Implemented shrinking of leaderboard tab so it doesn't overlap with resize handle in case of little vertical space --- .../gwt/ui/client/shared/racemap/RaceMap.java | 1 + .../sailing/gwt/ui/raceboard/RaceBoardPanel.java | 16 +++++++++++++++- .../ui/raceboard/SideBySideComponentViewer.java | 15 +++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) 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 c2b5e540db8..c315dec6d4e 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 @@ -2452,6 +2452,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe 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); 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 f54680eb93f..01b3a83826e 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 @@ -7,6 +7,8 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.core.shared.GWT; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Style.Unit; @@ -168,7 +170,19 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi raceMapResources.combinedWindPanelStyle().ensureInjected(); raceMap = new RaceMap(sailingService, asyncActionsExecutor, errorReporter, timer, competitorSelectionProvider, stringMessages, showMapControls, getConfiguration().isShowViewStreamlets(), getConfiguration().isShowViewStreamletColors(), getConfiguration().isShowViewSimulation(), - selectedRaceIdentifier, raceMapResources.combinedWindPanelStyle(), /* showHeaderPanel */ true); + selectedRaceIdentifier, raceMapResources.combinedWindPanelStyle(), /* showHeaderPanel */ true) { + @Override + public void onResize() { + super.onResize(); + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + // Show/hide the leaderboard panels toggle button text based on the race map height + leaderboardAndMapViewer.setLeftComponentToogleButtonTextVisibility(raceMap.getOffsetHeight() > 400); + } + }); + } + }; CompetitorFilterPanel competitorSearchTextBox = new CompetitorFilterPanel(competitorSelectionProvider, stringMessages, raceMap, new LeaderboardFetcher() { @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java index acab617a507..ae4827abd9f 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java @@ -3,6 +3,7 @@ package com.sap.sailing.gwt.ui.raceboard; import java.util.ArrayList; import java.util.List; +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; @@ -311,4 +312,18 @@ public class SideBySideComponentViewer implements ComponentViewer, UserStatusEve } } } + + /** + * Shows/hides the text on left components toggle button by modifying CSS font-size property. + * + * @param visible true to show the button text, false to hide it + */ + void setLeftComponentToogleButtonTextVisibility(final boolean visible) { + Splitter leftScrollPanelSplitter = splitLayoutPanel.getAssociatedSplitter(leftScrollPanel); + if (leftScrollPanelSplitter != null) { + Style toggleButtonStyle = leftScrollPanelSplitter.getToggleButton().getElement().getStyle(); + if (visible) toggleButtonStyle.clearFontSize(); + else toggleButtonStyle.setFontSize(0, Unit.PX); + } + } } From ae089e2350347f8acc3e567b49ba73e349f1ac9a Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 25 Nov 2015 16:15:56 +0100 Subject: [PATCH 039/306] changed RefreshableMultiSelectionModel.refeshSelec --- .../RefreshableMultiSelectionModel.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 2aadb13c699..3afb6eca890 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -32,17 +32,20 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im @Override public void refreshSelectionModel(Iterable newObjects) { final Set selectedSet = new HashSet<>(getSelectedSet()); - final boolean isSelected = !selectedSet.isEmpty(); + final boolean isNotEmpty = !selectedSet.isEmpty(); clear(); - for (final T it : newObjects) { - if (isSelected) { - for (final T selected : selectedSet) { - setSelected(it, comp == null ? selected.equals(it) : comp.representSameEntity(selected, it)); + if (isNotEmpty) { + for (T it : newObjects) { + boolean isSelected = false; + for (T selected : selectedSet) { + isSelected = (comp == null ? selected.equals(it) : comp.representSameEntity(selected, it)); + if (isSelected) { + break; + } } - } else { - setSelected(it, false); + setSelected(it, isSelected); } + scheduleSelectionChangeEvent(); } - scheduleSelectionChangeEvent(); } } \ No newline at end of file From dc90076e3143ae24238bba29bbaa5ad61802b306 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 25 Nov 2015 16:25:40 +0100 Subject: [PATCH 040/306] deleted unnecessary comments --- .../ui/adminconsole/RegattaListComposite.java | 37 +--- .../adminconsole/RegattaManagementPanel.java | 2 +- .../RegattaSelectionChangeListener.java | 23 --- .../gwt/ui/client/RegattaSelectionModel.java | 180 ------------------ .../ui/client/RegattaSelectionProvider.java | 28 --- 5 files changed, 4 insertions(+), 266 deletions(-) delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionChangeListener.java delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionProvider.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index e1ac6d1da32..c33ab3dca19 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -50,7 +50,6 @@ import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; * */ public class RegattaListComposite extends Composite implements RegattasDisplayer { -// protected final MultiSelectionModel regattaSelectionModel; protected final CellTable regattaTable; protected final ListDataProvider regattaListDataProvider; private List allRegattas; @@ -115,23 +114,7 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer }; filterablePanelRegattas.getTextBox().ensureDebugId("RegattasFilterTextBox"); panel.add(filterablePanelRegattas); -/* - @SuppressWarnings("unchecked") - MultiSelectionModel multiSelectionModel = (MultiSelectionModel) regattaTable.getSelectionModel(); - regattaSelectionModel = multiSelectionModel; -*/ - this.refreshableRegattaMultiSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { - @Override - public void onSelectionChange(SelectionChangeEvent event) { -/* List selectedRegattas = getSelectedRegattas(); - List selectedRaceIdentifiers = new ArrayList(); - for (RegattaDTO selectedRegatta : selectedRegattas) { - selectedRaceIdentifiers.add(selectedRegatta.getRegattaIdentifier()); - } - RegattaListComposite.this.regattaSelectionProvider.setSelection(selectedRaceIdentifiers);*/ - refreshableRegattaMultiSelectionModel.refreshSelectionModel(allRegattas); - } - }); + panel.add(regattaTable); initWidget(mainPanel); } @@ -347,17 +330,6 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer } protected List getSelectedRegattas() { - /* - List result = new ArrayList(); - if (regattaListDataProvider != null) { - for (RegattaDTO regatta : regattaListDataProvider.getList()) { - if (refreshableRegattaMultiSelectionModel.isSelected(regatta)) { - result.add(regatta); - } - } - } - return result; - */ return new ArrayList(refreshableRegattaMultiSelectionModel.getSelectedSet()); } @@ -372,13 +344,10 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer } List newAllRegattas = new ArrayList(); Util.addAll(regattas, newAllRegattas); - /*List newAllRegattaIdentifiers = new ArrayList(); - for (RegattaDTO regatta : regattas) { - newAllRegattaIdentifiers.add(regatta.getRegattaIdentifier()); - }*/ + allRegattas = newAllRegattas; filterablePanelRegattas.updateAll(allRegattas); - //regattaSelectionProvider.setAllRegattas(newAllRegattaIdentifiers); + refreshableRegattaMultiSelectionModel.refreshSelectionModel(regattas); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java index 79d90d5b05b..4c83d4e185d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java @@ -41,7 +41,7 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; * @author Frank Mittag (C5163974) * */ -public class RegattaManagementPanel extends SimplePanel implements RegattasDisplayer/*, RegattaSelectionChangeListener*/ { +public class RegattaManagementPanel extends SimplePanel implements RegattasDisplayer { private final SailingServiceAsync sailingService; private final ErrorReporter errorReporter; private final StringMessages stringMessages; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionChangeListener.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionChangeListener.java deleted file mode 100644 index a5cf497597e..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionChangeListener.java +++ /dev/null @@ -1,23 +0,0 @@ -/*package com.sap.sailing.gwt.ui.client; - -import java.util.List; - -import com.sap.sailing.domain.common.RegattaIdentifier; - -*//** - * Allows UI components to observe a regatta selector, such as a drop-down box showing a list of regattas - * - * @author Axel Uhl (d043530) - * - *//* -public interface RegattaSelectionChangeListener { - - *//** - * The first element is the first one selected - * - * @param selectedRegattas a non-null list which is empty if nothing is selected - *//* - void onRegattaSelectionChange(List selectedRegattas); - -} -*/ \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java deleted file mode 100644 index ed0c48029bd..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionModel.java +++ /dev/null @@ -1,180 +0,0 @@ -/*package com.sap.sailing.gwt.ui.client; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -import com.google.gwt.event.shared.GwtEvent; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.view.client.SelectionChangeEvent.Handler; -import com.sap.sailing.domain.common.RegattaIdentifier; -import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; -import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; - -public class RegattaSelectionModel implements RegattaSelectionProvider, RefreshableSelectionModel { - private final List selection; - - private final List allRegattas; - - private final Set listeners; - - private final boolean hasMultiSelection; - - public RegattaSelectionModel(boolean hasMultiSelection) { - this.hasMultiSelection = hasMultiSelection; - this.selection = new ArrayList(); - this.allRegattas = new ArrayList(); - listeners = new HashSet(); - } - - @Override - public List getSelectedRegattas() { - return Collections.unmodifiableList(selection); - } - - @Override - public void addRegattaSelectionChangeListener(RegattaSelectionChangeListener listener) { - listeners.add(listener); - } - - @Override - public void removeRegattaSelectionChangeListener(RegattaSelectionChangeListener listener) { - listeners.remove(listener); - } - - @Override - public void setSelection(List newSelection, RegattaSelectionChangeListener... listenersNotToNotify) { - boolean notify = !selection.equals(newSelection); - selection.clear(); - selection.addAll(newSelection); - if (notify) { - notifyListeners(listenersNotToNotify); - } - } - - private void notifyListeners(RegattaSelectionChangeListener[] listenersNotToNotify) { - List selectedRegattas = getSelectedRegattas(); - for (RegattaSelectionChangeListener listener : listeners) { - if (listenersNotToNotify == null || !Arrays.asList(listenersNotToNotify).contains(listener)) { - listener.onRegattaSelectionChange(selectedRegattas); - } - } - } - - *//** - * Sets the universe of all regattas from which this selection model may select. An equal list will subsequently be - * returned by {@link #getAllRegattas()}. Regattas from the {@link #getSelectedRegattas() selection} not in - * newAllRegattas are removed from the selection. If this happens, the selection listeners are notified. - *//* - @Override - public void setAllRegattas(List newAllRegattas, RegattaSelectionChangeListener... listenersNotToNotify) { - allRegattas.clear(); - for (RegattaIdentifier r : newAllRegattas) { - allRegattas.add(r); - } - for (Iterator i=selection.iterator(); i.hasNext(); ) { - RegattaIdentifier selectedRegatta = i.next(); - if (!allRegattas.contains(selectedRegatta)) { - i.remove(); - } - } - // when setting all regattas, the actual RegattaDTOs will usually have changed their identity and maybe also their state; - // so notifying the selection listeners is necessary anyhow - notifyListeners(listenersNotToNotify); - } - - @Override - public List getAllRegattas() { - return Collections.unmodifiableList(allRegattas); - } - - @Override - public boolean hasMultiSelection() { - return hasMultiSelection; - } - - *//** - * Returns an instance of HasEqualIdentity<RegattaIdentifier>. The compare - * method works the same way as o1.eqauls(o2). - *//* - @Override - public EntityIdentityComparator getEntityIdentityComparator() { - return new EntityIdentityComparator() { - @Override - public boolean representSameEntity(RegattaIdentifier dto1, RegattaIdentifier dto2) { - return dto1.equals(dto2); - } - }; - } - - @Override - public Set getSelectedSet() { - Iterable selected = getSelectedRegattas(); - Set result = new HashSet<>(); - for (RegattaIdentifier it : selected) { - result.add(it); - } - return result; - } - - @Override - public void clear() { - setAllRegattas(new ArrayList()); - } - - @Override - public void setSelected(RegattaIdentifier item, boolean selected) { - if (selected) { - List all = getAllRegattas(); - List selection = getSelectedRegattas(); - all.add(item); - selection.add(item); - setAllRegattas(all); - setSelection(selection); - } else { - List all = getAllRegattas(); - all.add(item); - setAllRegattas(all); - } - } - - @Override - public void refreshSelectionModel(Iterable newObjects) { - if(newObjects != null) { - List all = new ArrayList<>(); - for(RegattaIdentifier it : newObjects) { - all.add(it); - } - setAllRegattas(all); - } - } - - @Override - public HandlerRegistration addSelectionChangeHandler(Handler handler) { - // TODO Auto-generated method stub - return null; - } - - @Override - public boolean isSelected(RegattaIdentifier object) { - // TODO Auto-generated method stub - return false; - } - - @Override - public void fireEvent(GwtEvent event) { - // TODO Auto-generated method stub - - } - - @Override - public Object getKey(RegattaIdentifier item) { - // TODO Auto-generated method stub - return null; - } -} -*/ \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionProvider.java deleted file mode 100644 index 4ac30d35b9b..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RegattaSelectionProvider.java +++ /dev/null @@ -1,28 +0,0 @@ -/*package com.sap.sailing.gwt.ui.client; - -import java.util.List; - -import com.sap.sailing.domain.common.RegattaIdentifier; - -public interface RegattaSelectionProvider { - *//** - * @return a non-null list which may be empty - *//* - List getSelectedRegattas(); - - void addRegattaSelectionChangeListener(RegattaSelectionChangeListener listener); - - void removeRegattaSelectionChangeListener(RegattaSelectionChangeListener listener); - - void setSelection(List newSelection, RegattaSelectionChangeListener... listenersNotToNotify); - - boolean hasMultiSelection(); - - *//** - * @return a non-null list which may be empty - *//* - List getAllRegattas(); - - void setAllRegattas(List newAllRegattas, RegattaSelectionChangeListener... listenersNotToNotify); -} -*/ \ No newline at end of file From 3dbbe1c0ad73eb29fc21c2acb60d53e855c01a02 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 25 Nov 2015 16:52:45 +0100 Subject: [PATCH 041/306] some formating --- .../sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java | 2 -- .../gwt/ui/adminconsole/StructureImportManagementPanel.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index c33ab3dca19..92261297001 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -344,10 +344,8 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer } List newAllRegattas = new ArrayList(); Util.addAll(regattas, newAllRegattas); - allRegattas = newAllRegattas; filterablePanelRegattas.updateAll(allRegattas); - refreshableRegattaMultiSelectionModel.refreshSelectionModel(regattas); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java index 01a5a0c7c66..dbeb7ed0350 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java @@ -117,7 +117,6 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat VerticalPanel mainPanel = new VerticalPanel(); setWidget(mainPanel); mainPanel.setWidth("100%"); - createUI(mainPanel); regattaListComposite.addSelectionChangeHandler(new Handler() { @@ -127,7 +126,6 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat updateImportRegattasButtonEnabledness(); } }); - regattaStructureGrid = new FlexTable(); } From 945ad486270809cf90f28bf0582e9b4ea53807fe Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 25 Nov 2015 17:09:00 +0100 Subject: [PATCH 042/306] Fixed positioning of wind and map orientation controls so that they don't overlap with leaderboard handle and tab --- java/com.sap.sailing.gwt.ui/RaceBoard.css | 8 ++++++ .../gwt/ui/client/shared/racemap/RaceMap.java | 25 +++++++++++++++++-- .../gwt/ui/raceboard/RaceBoardPanel.java | 16 +++++++++++- .../raceboard/SideBySideComponentViewer.java | 11 +++++--- 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/RaceBoard.css b/java/com.sap.sailing.gwt.ui/RaceBoard.css index 4b9a54e6bf4..4b9d0f25dae 100644 --- a/java/com.sap.sailing.gwt.ui/RaceBoard.css +++ b/java/com.sap.sailing.gwt.ui/RaceBoard.css @@ -94,6 +94,14 @@ table { padding-top: 10px; padding-left: 10px; } +.CombinedWindPanelParentDiv.indentsmall, +.TrueNorthIndicatorPanelParentDiv.indentsmall { + padding-left: 30px; +} +.CombinedWindPanelParentDiv.indentbig, +.TrueNorthIndicatorPanelParentDiv.indentbig { + padding-left: 45px; +} .MapSimulationLegendParentDiv { padding-bottom: 10px; 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 c315dec6d4e..2b215c3317b 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 @@ -536,9 +536,8 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe RaceMap.this.add(sapLogo); } map.setControls(ControlPosition.LEFT_TOP, combinedWindPanel); - combinedWindPanel.getParent().addStyleName("CombinedWindPanelParentDiv"); map.setControls(ControlPosition.LEFT_TOP, trueNorthIndicatorPanel); - trueNorthIndicatorPanel.getParent().addStyleName("TrueNorthIndicatorPanelParentDiv"); + adjustLeftControlsIndent(); RaceMap.this.raceMapImageManager.loadMapIcons(map); map.setSize("100%", "100%"); @@ -2456,6 +2455,21 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe 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 (combinedWindPanel.getParent() != null && trueNorthIndicatorPanel.getParent() != null) { + this.adjustLeftControlsIndent(); + } + } + + private void adjustLeftControlsIndent() { + combinedWindPanel.getParent().setStyleName("CombinedWindPanelParentDiv"); + trueNorthIndicatorPanel.getParent().setStyleName("TrueNorthIndicatorPanelParentDiv"); + String leftControlsIndentStyle = getLeftControlsIndentStyle(); + if (leftControlsIndentStyle != null) { + combinedWindPanel.getParent().addStyleName(leftControlsIndentStyle); + trueNorthIndicatorPanel.getParent().addStyleName(leftControlsIndentStyle); + } } @Override @@ -2604,4 +2618,11 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe } return mapOptions; } + + /** + * @return CSS style name to adjust the indent of left controls (combined wind and true north indicator). + */ + protected String getLeftControlsIndentStyle() { + return null; + } } 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 01b3a83826e..a3b5ee333c2 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 @@ -171,6 +171,8 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi raceMap = new RaceMap(sailingService, asyncActionsExecutor, errorReporter, timer, competitorSelectionProvider, stringMessages, showMapControls, getConfiguration().isShowViewStreamlets(), getConfiguration().isShowViewStreamletColors(), getConfiguration().isShowViewSimulation(), selectedRaceIdentifier, raceMapResources.combinedWindPanelStyle(), /* showHeaderPanel */ true) { + private static final String INDENT_SMALL_CONTROL_STYLE = "indentsmall"; + private static final String INDENT_BIG_CONTROL_STYLE = "indentbig"; @Override public void onResize() { super.onResize(); @@ -178,10 +180,22 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi @Override public void execute() { // Show/hide the leaderboard panels toggle button text based on the race map height - leaderboardAndMapViewer.setLeftComponentToogleButtonTextVisibility(raceMap.getOffsetHeight() > 400); + leaderboardAndMapViewer.setLeftComponentToggleButtonTextVisibilityAndDraggerPosition(raceMap.getOffsetHeight() > 400); } }); } + + @Override + protected String getLeftControlsIndentStyle() { + // Calculate style name for left control indent based on race map height an leaderboard panel visibility + if (raceMap.getOffsetHeight() <= 300) { + return INDENT_BIG_CONTROL_STYLE; + } + if (leaderboardPanel.isVisible() && raceMap.getOffsetHeight() <= 500) { + return INDENT_SMALL_CONTROL_STYLE; + } + return super.getLeftControlsIndentStyle(); + } }; CompetitorFilterPanel competitorSearchTextBox = new CompetitorFilterPanel(competitorSelectionProvider, stringMessages, raceMap, new LeaderboardFetcher() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java index ae4827abd9f..6ff63bfde4b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java @@ -314,16 +314,21 @@ public class SideBySideComponentViewer implements ComponentViewer, UserStatusEve } /** - * Shows/hides the text on left components toggle button by modifying CSS font-size property. + * Shows/hides the text on left components toggle button by modifying CSS font-size property and adjust + * the dragger position by modifying CSS margin-top property. * - * @param visible true to show the button text, false to hide it + * @param visible + * true to show the button text, false to hide it */ - void setLeftComponentToogleButtonTextVisibility(final boolean visible) { + void setLeftComponentToggleButtonTextVisibilityAndDraggerPosition(final boolean visible) { Splitter leftScrollPanelSplitter = splitLayoutPanel.getAssociatedSplitter(leftScrollPanel); if (leftScrollPanelSplitter != null) { Style toggleButtonStyle = leftScrollPanelSplitter.getToggleButton().getElement().getStyle(); if (visible) toggleButtonStyle.clearFontSize(); else toggleButtonStyle.setFontSize(0, Unit.PX); + Style drapperStyle = leftScrollPanelSplitter.getDragger().getElement().getStyle(); + if (visible) drapperStyle.clearMarginTop(); + else drapperStyle.setMarginTop(-25, Unit.PX); } } } From 6912ae8d3420210de9fe2ca87cd9fb8421dbb919 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 25 Nov 2015 17:51:29 +0100 Subject: [PATCH 043/306] added a few TODOs for Lukas --- .../client/celltable/RefreshableMultiSelectionModel.java | 4 ++-- .../sse/gwt/client/celltable/RefreshableSelectionModel.java | 6 ++++-- .../client/celltable/RefreshableSingleSelectionModel.java | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 3afb6eca890..2d306f34cdd 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -7,7 +7,7 @@ import com.google.gwt.view.client.MultiSelectionModel; import com.google.gwt.view.client.ProvidesKey; public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel { - private EntityIdentityComparator comp; + private final EntityIdentityComparator comp; public RefreshableMultiSelectionModel() { super(); @@ -31,7 +31,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im @Override public void refreshSelectionModel(Iterable newObjects) { - final Set selectedSet = new HashSet<>(getSelectedSet()); + final Set selectedSet = new HashSet<>(getSelectedSet()); // TODO Lukas: a copy doesn't seem required (anymore?) here; the result of getSelectedSet() is not referenced beyond the scope of this method final boolean isNotEmpty = !selectedSet.isEmpty(); clear(); if (isNotEmpty) { diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java index a77a1c7d653..888a7144da5 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java @@ -5,11 +5,13 @@ import com.google.gwt.view.client.SetSelectionModel; import com.google.gwt.view.client.SingleSelectionModel; /** - * The Interface {@link RefreshableSelectionModel} unify the refresh behavior of SelectionModels e.g. of a + * The interface {@link RefreshableSelectionModel} unifies the refresh behavior of SelectionModels e.g. of a * {@link SingleSelectionModel}. To refresh this SelectionModel you can call the * {@link RefreshableSelectionModel#refreshSelectionModel(Iterable)} method and this method will refresh the * SelectionModel. * + * TODO Lukas, please enhance this comment to explain how this re-establishes the invariant of same elements in SetSelectionModel.getSelectedElements and DataProvider + * * @author D064976 */ @@ -27,7 +29,7 @@ public interface RefreshableSelectionModel extends SetSelectionModel { * has no {@link EntityIdentityComparator} set, this method will use the {@link #equals(Object)} method to compare. *

    * - * If an selected object is not in the newObjects the object wouldn't be selected anymore. + * If a selected object is not contained in newObjects the object wouldn't be selected anymore. *

    * * When the selection is refreshed this method triggers a diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index b8e71daf722..eb460ffc7a3 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -7,7 +7,7 @@ import com.google.gwt.view.client.ProvidesKey; import com.google.gwt.view.client.SingleSelectionModel; public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel { - private EntityIdentityComparator comp; + private final EntityIdentityComparator comp; public RefreshableSingleSelectionModel() { super(); @@ -31,14 +31,14 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel @Override public void refreshSelectionModel(Iterable newObjects) { - final Set selectedSet = new HashSet<>(getSelectedSet()); + final Set selectedSet = new HashSet<>(getSelectedSet()); // TODO Lukas: a copy doesn't seem required (anymore?) here; the result of getSelectedSet() is not referenced beyond the scope of this method final T selected = selectedSet.isEmpty() ? null : selectedSet.iterator().next(); clear(); for (final T it : newObjects) { if (selected != null) { setSelected(it, comp == null ? selected.equals(it) : comp.representSameEntity(selected, it)); } else { - setSelected(it, false); + setSelected(it, false); // TODO Lukas: clear() already clears the selection } } scheduleSelectionChangeEvent(); From 45b1a8186967f979f5ec1b21857b5b638f185fd4 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 25 Nov 2015 18:20:13 +0100 Subject: [PATCH 044/306] newSelectionModel: revised SelectionModels --- .../RefreshableMultiSelectionModel.java | 3 +-- .../RefreshableSingleSelectionModel.java | 17 +++++++---------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 2d306f34cdd..08631eaa268 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -1,6 +1,5 @@ package com.sap.sse.gwt.client.celltable; -import java.util.HashSet; import java.util.Set; import com.google.gwt.view.client.MultiSelectionModel; @@ -31,7 +30,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im @Override public void refreshSelectionModel(Iterable newObjects) { - final Set selectedSet = new HashSet<>(getSelectedSet()); // TODO Lukas: a copy doesn't seem required (anymore?) here; the result of getSelectedSet() is not referenced beyond the scope of this method + final Set selectedSet = getSelectedSet(); final boolean isNotEmpty = !selectedSet.isEmpty(); clear(); if (isNotEmpty) { diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index eb460ffc7a3..7c2095d90cc 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -1,8 +1,5 @@ package com.sap.sse.gwt.client.celltable; -import java.util.HashSet; -import java.util.Set; - import com.google.gwt.view.client.ProvidesKey; import com.google.gwt.view.client.SingleSelectionModel; @@ -31,14 +28,14 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel @Override public void refreshSelectionModel(Iterable newObjects) { - final Set selectedSet = new HashSet<>(getSelectedSet()); // TODO Lukas: a copy doesn't seem required (anymore?) here; the result of getSelectedSet() is not referenced beyond the scope of this method - final T selected = selectedSet.isEmpty() ? null : selectedSet.iterator().next(); + final T selected = getSelectedObject(); clear(); - for (final T it : newObjects) { - if (selected != null) { - setSelected(it, comp == null ? selected.equals(it) : comp.representSameEntity(selected, it)); - } else { - setSelected(it, false); // TODO Lukas: clear() already clears the selection + if (selected != null) { + for (final T it : newObjects) { + boolean isEqual = comp == null ? selected.equals(it) : comp.representSameEntity(selected, it); + if (isEqual) { + setSelected(it, true); + } } } scheduleSelectionChangeEvent(); From cfd8ded447516f2cf1a3c996cd4921b3d9bf66f0 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Thu, 26 Nov 2015 08:40:14 +0100 Subject: [PATCH 045/306] replaced EventSelectionModel --- .../ui/adminconsole/EventListComposite.java | 49 ++-------- .../ui/adminconsole/EventManagementPanel.java | 65 ++++++++------ .../client/EventSelectionChangeListener.java | 22 ----- .../gwt/ui/client/EventSelectionModel.java | 90 ------------------- .../gwt/ui/client/EventSelectionProvider.java | 26 ------ .../RefreshableMultiSelectionModel.java | 3 +- .../RefreshableSingleSelectionModel.java | 3 +- 7 files changed, 49 insertions(+), 209 deletions(-) delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionChangeListener.java delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionModel.java delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionProvider.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 61e51ed6d6b..18548f653c6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -33,7 +33,6 @@ import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.SelectionChangeEvent; import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; -import com.sap.sailing.gwt.ui.client.EventSelectionProvider; import com.sap.sailing.gwt.ui.client.EventsRefresher; import com.sap.sailing.gwt.ui.client.LeaderboardGroupsDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -48,7 +47,6 @@ import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -60,12 +58,11 @@ import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; */ public class EventListComposite extends Composite implements EventsRefresher, LeaderboardGroupsDisplayer { private final SailingServiceAsync sailingService; - private final EventSelectionProvider eventSelectionProvider; private final ErrorReporter errorReporter; private final StringMessages stringMessages; private CellTable eventTable; - private RefreshableMultiSelectionModel eventSelectionModel; + private RefreshableMultiSelectionModel refreshableEventSelectionModel; private ListDataProvider eventListDataProvider; private List allEvents; private LabeledAbstractFilterablePanel filterTextbox; @@ -93,10 +90,10 @@ public class EventListComposite extends Composite implements EventsRefresher, Le private final AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); - public EventListComposite(final SailingServiceAsync sailingService, final EventSelectionProvider eventSelectionProvider, + public EventListComposite(final SailingServiceAsync sailingService, final RefreshableMultiSelectionModel refreshableEventSelectionModel, final ErrorReporter errorReporter, final StringMessages stringMessages) { this.sailingService = sailingService; - this.eventSelectionProvider = eventSelectionProvider; + this.refreshableEventSelectionModel = refreshableEventSelectionModel; this.stringMessages = stringMessages; this.errorReporter = errorReporter; availableLeaderboardGroups = Collections.emptyList(); @@ -134,7 +131,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le @Override public void onClick(ClickEvent event) { if (Window.confirm(stringMessages.doYouReallyWantToRemoveEvents())) { - removeEvents(eventSelectionModel.getSelectedSet()); + removeEvents(refreshableEventSelectionModel.getSelectedSet()); } } }); @@ -145,22 +142,11 @@ public class EventListComposite extends Composite implements EventsRefresher, Le eventTable.ensureDebugId("EventsCellTable"); eventTable.setVisible(false); - @SuppressWarnings("unchecked") - RefreshableMultiSelectionModel multiSelectionModel = (RefreshableMultiSelectionModel) eventTable.getSelectionModel(); - eventSelectionModel = multiSelectionModel; - - eventSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + this.refreshableEventSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { - final boolean somethingSelected = !eventSelectionModel.getSelectedSet().isEmpty(); + final boolean somethingSelected = !refreshableEventSelectionModel.getSelectedSet().isEmpty(); removeEventsButton.setEnabled(somethingSelected); - - List selectedEvents = getSelectedEvents(); - List selectedEventUUIDs = new ArrayList(); - for (EventDTO selectedEvent : selectedEvents) { - selectedEventUUIDs.add(selectedEvent.id); - } - EventListComposite.this.eventSelectionProvider.setSelection(selectedEventUUIDs); } }); @@ -198,14 +184,9 @@ public class EventListComposite extends Composite implements EventsRefresher, Le eventListDataProvider.addDataDisplay(table); table.setWidth("100%"); - SelectionCheckboxColumn eventSelectionCheckboxColumn = new SelectionCheckboxColumn(tableRes.cellTableStyle().cellTableCheckboxSelected(), + SelectionCheckboxColumn eventSelectionCheckboxColumn = new SelectionCheckboxColumn(refreshableEventSelectionModel, tableRes.cellTableStyle().cellTableCheckboxSelected(), tableRes.cellTableStyle().cellTableCheckboxDeselected(), - tableRes.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { - @Override - public boolean representSameEntity(EventDTO dto1, EventDTO dto2) { - return dto1.id.equals(dto2.id) ? true : false; - } - } /*entityIdentityComparator to create a RefreshableSelectionModel*/) { + tableRes.cellTableStyle().cellTableCheckboxColumnCell()) { @Override protected ListDataProvider getListDataProvider() { return eventListDataProvider; @@ -607,7 +588,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le eventTable.setVisible(true); noEventsLabel.setVisible(false); } - eventSelectionModel.refreshSelectionModel(events); + refreshableEventSelectionModel.refreshSelectionModel(events); allEvents.clear(); allEvents.addAll(events); filterTextbox.updateAll(allEvents); @@ -618,16 +599,4 @@ public class EventListComposite extends Composite implements EventsRefresher, Le public List getAllEvents() { return allEvents; } - - private List getSelectedEvents() { - List result = new ArrayList(); - if (eventListDataProvider != null) { - for (EventDTO Event : eventListDataProvider.getList()) { - if (eventSelectionModel.isSelected(Event)) { - result.add(Event); - } - } - } - return result; - } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventManagementPanel.java index fd6c47395b5..29a92361c82 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventManagementPanel.java @@ -1,14 +1,11 @@ package com.sap.sailing.gwt.ui.adminconsole; -import java.util.List; -import java.util.UUID; - +import java.util.Set; import com.google.gwt.user.client.ui.CaptionPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; -import com.sap.sailing.gwt.ui.client.EventSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.EventSelectionModel; -import com.sap.sailing.gwt.ui.client.EventSelectionProvider; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; import com.sap.sailing.gwt.ui.client.EventsRefresher; import com.sap.sailing.gwt.ui.client.LeaderboardGroupsDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -16,6 +13,8 @@ import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.EventDTO; import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; /** * Allows administrators to manage a sailing event. @@ -23,11 +22,11 @@ import com.sap.sse.gwt.client.ErrorReporter; * @author Frank Mittag (C5163974) * @author Axel Uhl (d043530) */ -public class EventManagementPanel extends SimplePanel implements EventsRefresher, LeaderboardGroupsDisplayer, EventSelectionChangeListener { +public class EventManagementPanel extends SimplePanel implements EventsRefresher, LeaderboardGroupsDisplayer { private EventListComposite eventListComposite; private EventDetailsComposite eventDetailsComposite; private final CaptionPanel eventsPanel; - private EventSelectionProvider eventSelectionProvider; + private final RefreshableMultiSelectionModel refreshableEventSelectionModel; public EventManagementPanel(final SailingServiceAsync sailingService, final ErrorReporter errorReporter, final StringMessages stringMessages) { @@ -40,10 +39,36 @@ public class EventManagementPanel extends SimplePanel implements EventsRefresher VerticalPanel eventsContentPanel = new VerticalPanel(); eventsPanel.setContentWidget(eventsContentPanel); - eventSelectionProvider = new EventSelectionModel(true); - eventSelectionProvider.addEventSelectionChangeListener(this); + refreshableEventSelectionModel = new RefreshableMultiSelectionModel<>(new EntityIdentityComparator() { - eventListComposite = new EventListComposite(sailingService, eventSelectionProvider, errorReporter, stringMessages); + @Override + public boolean representSameEntity(EventDTO dto1, EventDTO dto2) { + return dto1.id.equals(dto2.id); + } + }); + + refreshableEventSelectionModel.addSelectionChangeHandler(new Handler() { + + @Override + public void onSelectionChange(SelectionChangeEvent event) { + final Set selectedEvents = refreshableEventSelectionModel.getSelectedSet(); + if (selectedEvents.size() == 1 && eventListComposite.getAllEvents() != null) { + final EventDTO selectedEvent = selectedEvents.iterator().next(); + for (EventDTO eventDTO : eventListComposite.getAllEvents()) { + if (eventDTO.id.equals(selectedEvent.id)) { + eventDetailsComposite.setEvent(eventDTO); + eventDetailsComposite.setVisible(true); + break; + } + } + } else { + eventDetailsComposite.setEvent(null); + eventDetailsComposite.setVisible(false); + } + } + }); + + eventListComposite = new EventListComposite(sailingService, refreshableEventSelectionModel, errorReporter, stringMessages); eventListComposite.ensureDebugId("EventListComposite"); eventsContentPanel.add(eventListComposite); @@ -62,22 +87,4 @@ public class EventManagementPanel extends SimplePanel implements EventsRefresher public void fillLeaderboardGroups(Iterable leaderboardGroups) { eventListComposite.fillLeaderboardGroups(leaderboardGroups); } - - @Override - public void onEventSelectionChange(List selectedEvents) { - final UUID selectedEventUUID; - if (selectedEvents.size() == 1 && eventListComposite.getAllEvents() != null) { - selectedEventUUID = selectedEvents.get(0); - for (EventDTO eventDTO : eventListComposite.getAllEvents()) { - if (eventDTO.id.equals(selectedEventUUID)) { - eventDetailsComposite.setEvent(eventDTO); - eventDetailsComposite.setVisible(true); - break; - } - } - } else { - eventDetailsComposite.setEvent(null); - eventDetailsComposite.setVisible(false); - } - } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionChangeListener.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionChangeListener.java deleted file mode 100644 index 013b258e080..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionChangeListener.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.List; -import java.util.UUID; - - -/** - * Allows UI components to observe a saling event selector, such as a drop-down box showing a list of sailing events - * - * @author Axel Uhl (d043530) - * - */ -public interface EventSelectionChangeListener { - - /** - * The first element is the first one selected - * - * @param selectedEvents a non-null list which is empty if nothing is selected - */ - void onEventSelectionChange(List selectedEvents); - -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionModel.java deleted file mode 100644 index 56a23c7202d..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionModel.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.UUID; - -public class EventSelectionModel implements EventSelectionProvider { - private final List selection; - private final List allEvents; - private final Set listeners; - private final boolean hasMultiSelection; - - public EventSelectionModel(boolean hasMultiSelection) { - this.hasMultiSelection = hasMultiSelection; - this.selection = new ArrayList(); - this.allEvents = new ArrayList(); - listeners = new HashSet(); - } - - @Override - public List getSelectedEvents() { - return Collections.unmodifiableList(selection); - } - - @Override - public void addEventSelectionChangeListener(EventSelectionChangeListener listener) { - listeners.add(listener); - } - - @Override - public void removeEventSelectionChangeListener(EventSelectionChangeListener listener) { - listeners.remove(listener); - } - - @Override - public void setSelection(List newSelection, EventSelectionChangeListener... listenersNotToNotify) { - boolean notify = !selection.equals(newSelection); - selection.clear(); - selection.addAll(newSelection); - if (notify) { - notifyListeners(listenersNotToNotify); - } - } - - private void notifyListeners(EventSelectionChangeListener[] listenersNotToNotify) { - List selectedEvents = getSelectedEvents(); - for (EventSelectionChangeListener listener : listeners) { - if (listenersNotToNotify == null || !Arrays.asList(listenersNotToNotify).contains(listener)) { - listener.onEventSelectionChange(selectedEvents); - } - } - } - - /** - * Sets the universe of all (sailing) events from which this selection model may select. An equal list will subsequently be - * returned by {@link #getallEvents()}. Events from the {@link #getSelectedEvents() selection} not in - * newEvents are removed from the selection. If this happens, the selection listeners are notified. - */ - @Override - public void setAllEvents(List newEvents, EventSelectionChangeListener... listenersNotToNotify) { - allEvents.clear(); - for (UUID r : newEvents) { - allEvents.add(r); - } - for (Iterator i=selection.iterator(); i.hasNext(); ) { - UUID selectedRegatta = i.next(); - if (!allEvents.contains(selectedRegatta)) { - i.remove(); - } - } - // when setting all events, the actual EventDTOs will usually have changed their identity and maybe also their state; - // so notifying the selection listeners is necessary anyhow - notifyListeners(listenersNotToNotify); - } - - @Override - public List getAllEvents() { - return Collections.unmodifiableList(allEvents); - } - - @Override - public boolean hasMultiSelection() { - return hasMultiSelection; - } -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionProvider.java deleted file mode 100644 index d4b9e197934..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/EventSelectionProvider.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.List; -import java.util.UUID; - -public interface EventSelectionProvider { - /** - * @return a non-null list which may be empty - */ - List getSelectedEvents(); - - void addEventSelectionChangeListener(EventSelectionChangeListener listener); - - void removeEventSelectionChangeListener(EventSelectionChangeListener listener); - - void setSelection(List newSelection, EventSelectionChangeListener... listenersNotToNotify); - - boolean hasMultiSelection(); - - /** - * @return a non-null list which may be empty - */ - List getAllEvents(); - - void setAllEvents(List events, EventSelectionChangeListener... listenersNotToNotify); -} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 08631eaa268..4ee2b99fe18 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -44,7 +44,8 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im } setSelected(it, isSelected); } - scheduleSelectionChangeEvent(); + setEventScheduled(true); + fireSelectionChangeEvent(); } } } \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index 7c2095d90cc..9907c58c899 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -38,6 +38,7 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel } } } - scheduleSelectionChangeEvent(); + setEventScheduled(true); + fireSelectionChangeEvent(); } } \ No newline at end of file From 4c4734647dfc7731bd0e51005745ddd71f1facd3 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Thu, 26 Nov 2015 11:18:14 +0100 Subject: [PATCH 046/306] insert RefreshableSelectionModel in LeaderboardCon --- .../AbstractLeaderboardConfigPanel.java | 21 ++++++++++--------- .../adminconsole/LeaderboardConfigPanel.java | 12 +++++------ ...martphoneTrackingEventManagementPanel.java | 4 ++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index 1c24cbc4d20..7e0352c989d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -23,7 +23,6 @@ import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.VerticalPanel; 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.SetSelectionModel; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; @@ -51,6 +50,7 @@ import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; 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.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -83,7 +83,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement protected List availableLeaderboardList; - protected final MultiSelectionModel leaderboardSelectionModel; + protected final RefreshableMultiSelectionModel refreshableLeaderboardSelectionModel; protected final RaceSelectionProvider raceSelectionProvider; @@ -174,10 +174,10 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement leaderboardTable.ensureDebugId("AvailableLeaderboardsTable"); addColumnsToLeaderboardTableAndSetSelectionModel(leaderboardTable, tableRes); @SuppressWarnings("unchecked") - MultiSelectionModel multiSelectionModel = (MultiSelectionModel) leaderboardTable.getSelectionModel(); - leaderboardSelectionModel = multiSelectionModel; + RefreshableMultiSelectionModel refreshableMultiSelectionModel = (RefreshableMultiSelectionModel) leaderboardTable.getSelectionModel(); + refreshableLeaderboardSelectionModel = refreshableMultiSelectionModel; leaderboardTable.setWidth("100%"); - leaderboardSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + refreshableLeaderboardSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { public void onSelectionChange(SelectionChangeEvent event) { leaderboardSelectionChanged(); raceColumnTable.setSelectedLeaderboardName(getSelectedLeaderboardName()); @@ -288,7 +288,8 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement availableLeaderboardList.clear(); Util.addAll(leaderboards, availableLeaderboardList); filterLeaderboardPanel.updateAll(availableLeaderboardList); // also maintains the filtered leaderboardList - leaderboardSelectionChanged(); + //leaderboardSelectionChanged(); + refreshableLeaderboardSelectionModel.refreshSelectionModel(leaderboards); leaderboardRaceColumnSelectionChanged(); } @@ -302,15 +303,15 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement new AsyncCallback() { @Override public void onSuccess(StrippedLeaderboardDTO leaderboard) { - for (StrippedLeaderboardDTO leaderboardDTO : leaderboardSelectionModel.getSelectedSet()) { + for (StrippedLeaderboardDTO leaderboardDTO : refreshableLeaderboardSelectionModel.getSelectedSet()) { if (leaderboardDTO.name.equals(leaderboardName)) { - leaderboardSelectionModel.setSelected(leaderboardDTO, false); + refreshableLeaderboardSelectionModel.setSelected(leaderboardDTO, false); break; } } replaceLeaderboardInList(availableLeaderboardList, leaderboardName, leaderboard); filterLeaderboardPanel.updateAll(availableLeaderboardList); // also updates leaderboardList provider - leaderboardSelectionModel.setSelected(leaderboard, true); + refreshableLeaderboardSelectionModel.setSelected(leaderboard, true); if (nameOfRaceColumnToSelect != null) { selectRaceColumn(nameOfRaceColumnToSelect); } @@ -522,7 +523,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement @Override public StrippedLeaderboardDTO getSelectedLeaderboard() { - return leaderboardSelectionModel.getSelectedSet().isEmpty() ? null : leaderboardSelectionModel.getSelectedSet().iterator().next(); + return refreshableLeaderboardSelectionModel.getSelectedSet().isEmpty() ? null : refreshableLeaderboardSelectionModel.getSelectedSet().iterator().next(); } protected LeaderboardsRefresher getLeaderboardsRefresher() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java index 61f4c85bfb2..0fa5839fa4e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java @@ -119,7 +119,7 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { @Override public void onClick(ClickEvent event) { if (Window.confirm(stringMessages.doYouReallyWantToRemoveLeaderboards())) { - removeLeaderboards(leaderboardSelectionModel.getSelectedSet()); + removeLeaderboards(refreshableLeaderboardSelectionModel.getSelectedSet()); } } }); @@ -683,9 +683,9 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { raceSelectionProvider.addRaceSelectionChangeListener(LeaderboardConfigPanel.this); } }); - leaderboardRemoveButton.setEnabled(!leaderboardSelectionModel.getSelectedSet().isEmpty()); + leaderboardRemoveButton.setEnabled(!refreshableLeaderboardSelectionModel.getSelectedSet().isEmpty()); StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard(); - if (leaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { + if (refreshableLeaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { raceColumnTable.getDataProvider().getList().clear(); for (RaceColumnDTO raceColumn : selectedLeaderboard.getRaceList()) { for (FleetDTO fleet : raceColumn.getFleets()) { @@ -785,8 +785,8 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { private void addLeaderboard(StrippedLeaderboardDTO result) { leaderboardList.getList().add(result); availableLeaderboardList.add(result); - leaderboardSelectionModel.clear(); - leaderboardSelectionModel.setSelected(result, true); + refreshableLeaderboardSelectionModel.clear(); + refreshableLeaderboardSelectionModel.setSelected(result, true); } private void updateLeaderboard(final String oldLeaderboardName, final LeaderboardDescriptor leaderboardToUpdate) { @@ -857,6 +857,6 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { private void removeLeaderboardFromTable(final StrippedLeaderboardDTO leaderBoard) { leaderboardList.getList().remove(leaderBoard); availableLeaderboardList.remove(leaderBoard); - leaderboardSelectionModel.setSelected(leaderBoard, false); + refreshableLeaderboardSelectionModel.setSelected(leaderBoard, false); } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java index 7d8bb340977..f4a96bd60e0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java @@ -373,7 +373,7 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC protected void leaderboardSelectionChanged() { StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard(); regattaHasCompetitors = false; - if (leaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { + if (refreshableLeaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { raceColumnTable.getDataProvider().getList().clear(); for (RaceColumnDTO raceColumn : selectedLeaderboard.getRaceList()) { for (FleetDTO fleet : raceColumn.getFleets()) { @@ -386,7 +386,7 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC trackedRacesCaptionPanel.setVisible(true); } - sailingService.doesRegattaLogContainCompetitors(((StrippedLeaderboardDTO) leaderboardSelectionModel.getSelectedSet().toArray()[0]).name, new RegattaLogCallBack()); + sailingService.doesRegattaLogContainCompetitors(((StrippedLeaderboardDTO) refreshableLeaderboardSelectionModel.getSelectedSet().toArray()[0]).name, new RegattaLogCallBack()); } else { selectedLeaderBoardPanel.setVisible(false); trackedRacesCaptionPanel.setVisible(false); From d20ace7c9c1be5772fe8493ffb1ceb4ba8bf0365 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 26 Nov 2015 11:27:24 +0100 Subject: [PATCH 047/306] Leaderboard in Race Viewer - Show boat color under SailId if Competitor Full Name is not shown --- .../gwt/ui/leaderboard/LeaderboardPanel.java | 6570 +++++++++-------- 1 file changed, 3290 insertions(+), 3280 deletions(-) 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 1f8a91c7bab..b8c843540e4 100755 --- 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,3280 +1,3290 @@ -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.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.VerticalPanel; -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.InvertibleComparator; -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.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.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.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.leaderboard.LeaderboardSettings.RaceColumnSelectionStrategies; -import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; -import com.sap.sse.common.Util; -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.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.controls.busyindicator.BusyIndicator; -import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; -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.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.useragent.UserAgentDetails; - -/** - * A leaderboard essentially consists of a table widget that in its columns displays the entries. - * - * @author Axel Uhl (D043530) - * - */ -public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayStateListener, DisplayedLeaderboardRowsProvider, - Component, IsEmbeddableComponent, CompetitorSelectionChangeListener, LeaderboardFetcher { - 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"; - - interface RaceColumnTemplates extends SafeHtmlTemplates { - @SafeHtmlTemplates.Template("

    ") - SafeHtml cellFrameWithTextColorAndFleetBorder(String textColor, String borderStyle); - - @SafeHtmlTemplates.Template("
    ") - SafeHtml cellFrameWithTextColor(String textColor); - - @SafeHtmlTemplates.Template("
    ") - SafeHtml cellWithImageResourceAndText(ImageResource image, String text); - } - - private static RaceColumnTemplates raceColumnTemplate = GWT.create(RaceColumnTemplates.class); - - /** - * 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 String leaderboardName; - - private final ErrorReporter errorReporter; - - private final StringMessages stringMessages; - - private final SortedCellTable 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; - - /** - * 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; - - /** - * 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; - - private final List selectedOverallDetailColumns; - - private final Map> overallDetailColumnMap; - - private RaceColumnSelection raceColumnSelection; - - protected final String RACE_COLUMN_HEADER_STYLE; - - protected final String LEG_DETAIL_COLUMN_HEADER_STYLE; - - protected final String LEG_DETAIL_COLUMN_STYLE; - - protected final String LEG_COLUMN_HEADER_STYLE; - - protected final String RACE_COLUMN_STYLE; - - protected final String LEG_COLUMN_STYLE; - - protected final String TOTAL_COLUMN_STYLE; - - 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 VerticalPanel contentPanel; - - private HorizontalPanel refreshAndSettingsPanel; - private Label scoreCorrectionLastUpdateTimeLabel; - private Label scoreCorrectionCommentLabel; - private Label liveRaceLabel; - - private boolean isEmbedded; - - private static final LeaderboardResources resources = GWT.create(LeaderboardResources.class); - private static final ComponentResources componentResources = GWT.create(ComponentResources.class); - private static final LeaderboardTableResources tableResources = GWT.create(LeaderboardTableResources.class); - - private ImageResource pauseIcon; - private ImageResource playIcon; - - private final BusyIndicator busyIndicator; - - /** - * Tells whether the leaderboard settings were explicitly changed by an external call to - * {@link #updateSettings(LeaderboardSettings)}. If so, a {@link #playStateChanged(PlayStates, PlayModes) play state - * change} will not automatically lead to a settings change. - */ - private boolean settingsUpdatedExplicitly = false; - - /** - * Tells if the leaderboard is currently handling a {@link #playStateChanged(PlayStates, PlayModes) play state - * change}. If this is the case, a call to {@link #updateSettings(LeaderboardSettings)} won't set the - * {@link #settingsUpdatedExplicitly} flag. - */ - private boolean currentlyHandlingPlayStateChange; - - private PlayModes oldPlayMode; - - 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; - - private boolean initialCompetitorFilterHasBeenApplied = false; - private final boolean showCompetitorFilterStatus; - - private CompetitorFilterPanel competitorFilterPanel; - - protected StringMessages getStringMessages() { - return stringMessages; - } - - @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 VerticalPanel getContentPanel() { - return contentPanel; - } - - protected ImageResource getSettingsIcon() { - return componentResources.settingsIcon(); - } - - public void updateSettings(final LeaderboardSettings newSettings) { - boolean oldShallAddOverallDetails = shallAddOverallDetails(); - if (newSettings.getOverallDetailsToShow() != null) { - selectedOverallDetailColumns.clear(); - selectedOverallDetailColumns.addAll(newSettings.getOverallDetailsToShow()); - } - if (!newSettings.isUpdateUponPlayStateChange() || !currentlyHandlingPlayStateChange) { - settingsUpdatedExplicitly = true; - } - 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.toggleExpansion(); - 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()); - } - // 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; - } - - final boolean oldBusyState = getBusyIndicator().isBusy(); - getBusyIndicator().setBusy(true); - Runnable doWhenNecessaryDetailHasBeenLoaded = new Runnable() { - @Override - public void run() { - setAutoExpandPreSelectedRace(false); // avoid expansion during updateLeaderboard(...); will expand later - // if it was expanded before - // update leaderboard after settings panel column selection change - updateLeaderboard(leaderboard); - setAutoExpandPreSelectedRace(newSettings.isAutoExpandPreSelectedRace()); - - if (newSettings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { - timer.setRefreshInterval(newSettings.getDelayBetweenAutoAdvancesInMilliseconds()); - } - for (ExpandableSortableColumn expandableSortableColumn : columnsToExpandAgain) { - expandableSortableColumn.toggleExpansion(); - } - if (newSettings.getNameOfRaceToSort() != null) { - final RaceColumn raceColumnByRaceName = getRaceColumnByRaceName(newSettings - .getNameOfRaceToSort()); - if (raceColumnByRaceName != null) { - getLeaderboardTable().sortColumn(raceColumnByRaceName, /* ascending */true); - } - } - getBusyIndicator().setBusy(oldBusyState); - } - }; - 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 */getNamesOfExpandedRaces(), - shallAddOverallDetails(), previousLeaderboard.getId(), /* fillNetPointsUncorrected */ 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; - } - - 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; - } - - @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(); - - 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()); - } - - @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.totalPoints != null) { - addedScores += entryBefore.totalPoints; - } - // else "add 0" - if (raceColumn.getName().equals(getRaceColumnName())) { - break; // we've reached the current column - stop here - } - } - return addedScores; - } - - /** - * Displays a combination of total points and maxPointsReason in bold, transparent, strike-through, depending on - * various criteria. Here's how: - * - *

    -         *                                  total 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 totalOrAddedPointsAsText = isShowAddedScores() ? addedScores - : entry.totalPoints == null ? "" : scoreFormat.format(entry.totalPoints); - String netOrAddedPointsAsText = isShowAddedScores() ? addedScores - : entry.netPoints == null ? "" : scoreFormat.format(entry.netPoints); - 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(totalOrAddedPointsAsText); - html.appendHtmlConstant(""); - } else { - html.appendHtmlConstant(" "); - html.appendHtmlConstant(netOrAddedPointsAsText); - 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_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_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, - DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL}; - } - - 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 totalPoints = object.fieldsByRaceColumnName.get(getRaceColumnName()).totalPoints; - return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); - } - - @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 CurrentSpeedOverGroundInKnots(), - 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) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null) { - double distanceTraveledInMeters = 0; - long timeInMilliseconds = 0; - for (LegEntryDTO legDetail : fieldsForRace.legDetails) { - if (legDetail != null) { - if (legDetail.distanceTraveledInMeters != null && legDetail.timeInMilliseconds != null) { - distanceTraveledInMeters += legDetail.distanceTraveledInMeters; - timeInMilliseconds += legDetail.timeInMilliseconds; - } else { - distanceTraveledInMeters = 0; - timeInMilliseconds = 0; - break; - } - } - } - if (timeInMilliseconds != 0) { - result = distanceTraveledInMeters / (double) timeInMilliseconds * 1000 * 3600 / 1852; - } - } - 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) { - 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.distanceTraveledInMeters != null) { - if (result == null) { - result = 0.0; - } - result += legDetail.distanceTraveledInMeters; - } - } - } - } - return result; - } - } - - 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 class CurrentSpeedOverGroundInKnots implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double 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) { - result = lastLegDetail.currentSpeedOverGroundInKnots; - } - } - } - 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.totalPoints != 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 totals for a competitor for the entire leaderboard. - * - * @author Axel Uhl (D043530) - * - */ - private class TotalsColumn extends LeaderboardSortableColumnWithMinMax { - private final String columnStyle; - - protected TotalsColumn(String columnStyle) { - super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); - this.columnStyle = columnStyle; - setHorizontalAlignment(ALIGN_CENTER); - } - - @Override - public String getValue(LeaderboardRowDTO object) { - Double totalPoints = object.totalPoints; - return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); - } - - @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.totalsColumnTooltip()); - } - } - - 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(tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()); - 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); - if (row != null) { - redrawRow(row); - } - } - - private void redrawRow(final LeaderboardRowDTO row) { - final List leaderboardDataList = getData().getList(); - synchronized (leaderboardDataList) { - redrawRow(row, leaderboardDataList); - } - } - - /** - * Ensure that the checkbox is redrawn when the competitor selection changes - */ - @Override - public void removedFromSelection(CompetitorDTO competitor) { - final LeaderboardRowDTO row = getRow(competitor); - if (row != null) { - redrawRow(row); - } - } - - @Override - public void updateMinMax() {} - - @Override - protected ListDataProvider getListDataProvider() { - return getData(); - } - } - - 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(); - } - } - - public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - LeaderboardSettings settings, CompetitorSelectionProvider competitorSelectionProvider, - String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, - final UserAgentDetails userAgent, boolean showRaceDetails) { - this(sailingService, asyncActionsExecutor, settings, false, /* preSelectedRace */null, competitorSelectionProvider, - null, leaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails); - } - - public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, - CompetitorSelectionProvider competitorSelectionProvider, String leaderboardGroupName, - String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, - final UserAgentDetails userAgent, boolean showRaceDetails) { - this(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 */3000l), leaderboardGroupName, - leaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails, - /* competitorSearchTextBox */ null, /* showSelectionCheckbox */ true, /* optionalRaceTimesInfoProvider */ null, - /* autoExpandLastRaceColumn */ false, /* adjustTimerDelay */ true, /*autoApplyTop30Filter*/ false, false); - } - - public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, - CompetitorSelectionProvider competitorSelectionProvider, Timer timer, String leaderboardGroupName, - String leaderboardName, final ErrorReporter errorReporter, final StringMessages stringMessages, - final UserAgentDetails userAgent, boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, - boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, - boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, boolean showCompetitorFilterStatus) { - this.setTitle(stringMessages.leaderboard()); - 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.selectedLegDetails = new ArrayList(); - this.selectedRaceDetails = new ArrayList(); - this.selectedOverallDetailColumns = new ArrayList(); - this.raceTimesInfoProvider = optionalRaceTimesInfoProvider; - this.selectedManeuverDetails = new ArrayList(); - this.adjustTimerDelay = adjustTimerDelay; - this.initialCompetitorFilterHasBeenApplied = !autoApplyTopNFilter; - this.showCompetitorFilterStatus = showCompetitorFilterStatus; - overallDetailColumnMap = createOverallDetailColumnMap(); - settingsUpdatedExplicitly = !settings.isUpdateUponPlayStateChange(); - this.leaderboardUpdateListener = new ArrayList(); - if (settings.getLegDetailsToShow() != null) { - selectedLegDetails.addAll(settings.getLegDetailsToShow()); - } - if (settings.getManeuverDetailsToShow() != null) { - selectedManeuverDetails.addAll(settings.getManeuverDetailsToShow()); - } - if (settings.getRaceDetailsToShow() != null) { - selectedRaceDetails.addAll(settings.getRaceDetailsToShow()); - } - if (settings.getOverallDetailsToShow() != null) { - selectedOverallDetailColumns.addAll(settings.getOverallDetailsToShow()); - } - setAutoExpandPreSelectedRace(settings.isAutoExpandPreSelectedRace()); - this.autoExpandLastRaceColumn = autoExpandLastRaceColumn; - if (settings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { - timer.setRefreshInterval(settings.getDelayBetweenAutoAdvancesInMilliseconds()); - } - this.timer = timer; - 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(); - selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider); - RACE_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableRaceColumnHeader(); - LEG_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegColumnHeader(); - LEG_DETAIL_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumnHeader(); - RACE_COLUMN_STYLE = tableResources.cellTableStyle().cellTableRaceColumn(); - LEG_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegColumn(); - LEG_DETAIL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumn(); - TOTAL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableTotalColumn(); - leaderboardTable = new SortedCellTableWithStylableHeaders( - /* pageSize */10000, tableResources); - 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"); - getLeaderboardTable().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()); - setShowAddedScores(settings.isShowAddedScores()); - setShowCompetitorSailId(settings.isShowCompetitorSailIdColumn()); - setShowCompetitorFullName(settings.isShowCompetitorFullNameColumn()); - setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(settings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()); - if (timer.isInitialized()) { - loadCompleteLeaderboard(getLeaderboardDisplayDate()); - } - - contentPanel = new VerticalPanel(); - leaderboardTable.getElement().getStyle().setMarginTop(10, Unit.PX); - contentPanel.setStyleName(STYLE_LEADERBOARD_CONTENT); - busyIndicator = new SimpleBusyIndicator(false, 0.8f); - busyIndicator.ensureDebugId("BusyIndicator"); - - - // the information panel - if (!isEmbedded) { - Widget toolbarPanel = createToolbarPanel(); - contentPanel.add(toolbarPanel); - } - if (competitorSearchTextBox != null) { - contentPanel.add(competitorSearchTextBox); - contentPanel.add(busyIndicator); - competitorSearchTextBox.getSettingsButton().addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - new SettingsDialog(LeaderboardPanel.this, stringMessages).show(); - } - }); - this.competitorFilterPanel = competitorSearchTextBox; - } - SortedCellTable leaderboardTable = getLeaderboardTable(); - // leaderboardTable.getElement().getStyle().setMarginTop(5, Unit.PX); - filterControlPanel = new HorizontalPanel(); - filterControlPanel.setStyleName("LeaderboardPanel-FilterControl-Panel"); - contentPanel.add(leaderboardTable); - if (showCompetitorFilterStatus) { - contentPanel.add(createFilterDeselectionControl()); - } - setWidget(contentPanel); - raceNameForDefaultSorting = settings.getNameOfRaceToSort(); - } - - 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; - } - - 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) { - getBusyIndicator().setBusy(true); - competitorFilterPanel.clearAllActiveFilters(); - timeChanged(new Date(), null); - } - }); - 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.toggleExpansion(); - // 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); - } - } - - private void loadCompleteLeaderboard(Date date) { - if (needsDataLoading()) { - GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction(sailingService, - getLeaderboardName(), useNullAsTimePoint() ? null : date, - /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(), shallAddOverallDetails(), /* previousLeaderboard */ - getLeaderboard(), isFillNetPointsUncorrected(), timer, errorReporter, stringMessages); - this.asyncActionsExecutor.execute(getLeaderboardByNameAction, LOAD_LEADERBOARD_DATA_CATEGORY, - new AsyncCallback() { - @Override - public void onSuccess(LeaderboardDTO result) { - updateLeaderboard(result); - getBusyIndicator().setBusy(false); - } - - @Override - public void onFailure(Throwable caught) { - getBusyIndicator().setBusy(false); - getErrorReporter() - .reportError("Error trying to obtain leaderboard contents: " + caught.getMessage(), - true /* silentMode */); - } - }); - } else { - getBusyIndicator().setBusy(false); - } - } - - protected boolean isFillNetPointsUncorrected() { - return false; - } - - /** - * 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; - } - - private boolean needsDataLoading() { - return isVisible(); - } - - /** - * Determine from column expansion state which races need their leg details - */ - private Collection getNamesOfExpandedRaces() { - Collection namesOfExpandedRaces = 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()) { - namesOfExpandedRaces.add(raceColumn.getRaceColumnName()); - } - } - } - return namesOfExpandedRaces; - } - - 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. - */ - protected void updateLeaderboard(LeaderboardDTO leaderboard) { - if (leaderboard != null) { - Collection> columnsToCollapseAndExpandAgain = getExpandedRaceColumnsWhoseDisplayedLegCountChanged(leaderboard); - for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { - columnToCollapseAndExpandAgain.toggleExpansion(); - } - 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.toggleExpansion(); - } - 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.toggleExpansion(); - 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(""); - } - - List> liveRaces = leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()); - boolean hasLiveRace = !liveRaces.isEmpty(); - liveRaceLabel.setText(hasLiveRace ? getLiveRacesText() : ""); - if (hasLiveRace) { - String liveRaceText = ""; - if(liveRaces.size() == 1) { - com.sap.sse.common.Util.Pair liveRace = liveRaces.get(0); - liveRaceText = stringMessages.raceIsLive("'" + liveRace.getA().getRaceColumnName() + "'"); - } else { - String raceNames = ""; - for (com.sap.sse.common.Util.Pair liveRace : liveRaces) { - raceNames += "'" + liveRace.getA().getRaceColumnName() + "', "; - } - // remove last ", " - raceNames = raceNames.substring(0, raceNames.length() - 2); - liveRaceText = stringMessages.racesAreLive(raceNames); - } - liveRaceLabel.setText(liveRaceText); - } else { - liveRaceLabel.setText(""); - } - scoreCorrectionLastUpdateTimeLabel.setVisible(!hasLiveRace); - liveRaceLabel.setVisible(hasLiveRace); - } - informLeaderboardUpdateListenersAboutLeaderboardUpdated(leaderboard); - getBusyIndicator().setBusy(false); - } - } - - /** - * 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 - */ - private Iterable getCompetitors(RaceIdentifier race) { - Set result = new HashSet(); - 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 (iraceColumn - */ - private void correctColumnData(RaceColumn raceColumn, RaceColumnDTO race) { - int columnIndex = getRaceColumnPosition(raceColumn); - if (raceColumn.isExpansionEnabled() != race.hasTrackedRaces() || race.isMedalRace() != raceColumn.isMedalRace()) { - if (raceColumn.isExpanded()) { - raceColumn.toggleExpansion(); // 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 TotalsColumn) { - 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, getStringMessages(), - 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 TotalsColumn)) { - addColumn(new TotalsColumn(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; - } - - protected 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(getLeaderboardDisplayDate()); - } - - @Override - public void playStateChanged(PlayStates playState, PlayModes playMode) { - currentlyHandlingPlayStateChange = true; - - if(!isEmbedded) { - playPause.setHTML(getPlayPauseImgHtml(playState)); - playPause.setTitle(playState == PlayStates.Playing ? stringMessages.pauseAutomaticRefresh() : stringMessages.autoRefresh()); - } - if (!settingsUpdatedExplicitly && playMode != oldPlayMode) { - // if settings weren't explicitly modified, auto-switch to live mode settings and sort for - // any pre-selected race; we need to copy the previously selected race columns to the new RaceColumnSelection - updateSettings(LeaderboardSettingsFactory.getInstance().createNewSettingsForPlayMode( - playMode, - /* don't touch columnToSort if no race was pre-selected */ preSelectedRace == null ? null - : preSelectedRace.getRaceName(), - /* don't change nameOfRaceColumnToShow */null, - /* set nameOfRaceToShow if race was pre-selected */preSelectedRace == null ? null : preSelectedRace - .getRaceName(), getRaceColumnSelection(), /* leave showRegattaRank and overall details unchanged */ null, - /* take into account state of competitor columns*/isShowCompetitorSailId(), isShowCompetitorFullName())); - } - currentlyHandlingPlayStateChange = false; - oldPlayMode = playMode; - } - - @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(Collections.unmodifiableList(selectedManeuverDetails), - Collections.unmodifiableList(selectedLegDetails), Collections.unmodifiableList(selectedRaceDetails), - Collections.unmodifiableList(selectedOverallDetailColumns), /* All races to select */ leaderboard.getRaceList(), - raceColumnSelection.getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard), raceColumnSelection, autoExpandPreSelectedRace, - isShowAddedScores(), timer.getRefreshInterval(), isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), - isShowCompetitorSailId(), isShowCompetitorFullName(), stringMessages); - } - - @Override - public String getLocalizedShortName() { - return stringMessages.leaderboard(); - } - - private LeaderboardRowDTO getRow(CompetitorDTO competitor) { - synchronized (getData().getList()) { - for (LeaderboardRowDTO row : getData().getList()) { - if (row.competitor.equals(competitor)) { - return row; - } - } - } - return null; - } - - @Override - public void addedToSelection(CompetitorDTO competitor) { - LeaderboardRowDTO row = getRow(competitor); - if (row != null) { - leaderboardSelectionModel.setSelected(row, true); - } - } - - @Override - public void removedFromSelection(CompetitorDTO competitor) { - LeaderboardRowDTO row = getRow(competitor); - if (row != null) { - leaderboardSelectionModel.setSelected(row, false); - } - } - - @Override - public BusyIndicator getBusyIndicator() { - return busyIndicator; - } - - @Override - public boolean hasBusyIndicator() { - return true; - } - - 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); - } - } - - public void addLeaderboardUpdateListener(LeaderboardUpdateListener listener) { - this.leaderboardUpdateListener.add(listener); - } - - protected void informLeaderboardUpdateListenersAboutLeaderboardUpdated(LeaderboardDTO leaderboard) { - for (LeaderboardUpdateListener listener : 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()); - if (!liveRaces.isEmpty()) { - if(liveRaces.size() == 1) { - com.sap.sse.common.Util.Pair liveRace = liveRaces.get(0); - result = stringMessages.raceIsLive("'" + liveRace.getA().getRaceColumnName() + "'"); - } else { - String raceNames = ""; - for (com.sap.sse.common.Util.Pair liveRace : liveRaces) { - raceNames += "'" + liveRace.getA().getRaceColumnName() + "', "; - } - // remove last ", " - raceNames = raceNames.substring(0, raceNames.length() - 2); - result = stringMessages.racesAreLive(raceNames); - } - } - return result; - } - - @Override - public String getDependentCssClassName() { - return "leaderboard"; - } -} +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.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.VerticalPanel; +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.InvertibleComparator; +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.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.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.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.leaderboard.LeaderboardSettings.RaceColumnSelectionStrategies; +import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; +import com.sap.sse.common.Util; +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.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.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; +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.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.useragent.UserAgentDetails; + +/** + * A leaderboard essentially consists of a table widget that in its columns displays the entries. + * + * @author Axel Uhl (D043530) + * + */ +public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayStateListener, DisplayedLeaderboardRowsProvider, + Component, IsEmbeddableComponent, CompetitorSelectionChangeListener, LeaderboardFetcher { + 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"; + + interface RaceColumnTemplates extends SafeHtmlTemplates { + @SafeHtmlTemplates.Template("

    ") + SafeHtml cellFrameWithTextColorAndFleetBorder(String textColor, String borderStyle); + + @SafeHtmlTemplates.Template("
    ") + SafeHtml cellFrameWithTextColor(String textColor); + + @SafeHtmlTemplates.Template("
    ") + SafeHtml cellWithImageResourceAndText(ImageResource image, String text); + } + + private static RaceColumnTemplates raceColumnTemplate = GWT.create(RaceColumnTemplates.class); + + /** + * 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 String leaderboardName; + + private final ErrorReporter errorReporter; + + private final StringMessages stringMessages; + + private final SortedCellTable 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; + + /** + * 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; + + /** + * 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; + + private final List selectedOverallDetailColumns; + + private final Map> overallDetailColumnMap; + + private RaceColumnSelection raceColumnSelection; + + protected final String RACE_COLUMN_HEADER_STYLE; + + protected final String LEG_DETAIL_COLUMN_HEADER_STYLE; + + protected final String LEG_DETAIL_COLUMN_STYLE; + + protected final String LEG_COLUMN_HEADER_STYLE; + + protected final String RACE_COLUMN_STYLE; + + protected final String LEG_COLUMN_STYLE; + + protected final String TOTAL_COLUMN_STYLE; + + 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 VerticalPanel contentPanel; + + private HorizontalPanel refreshAndSettingsPanel; + private Label scoreCorrectionLastUpdateTimeLabel; + private Label scoreCorrectionCommentLabel; + private Label liveRaceLabel; + + private boolean isEmbedded; + + private static final LeaderboardResources resources = GWT.create(LeaderboardResources.class); + private static final ComponentResources componentResources = GWT.create(ComponentResources.class); + private static final LeaderboardTableResources tableResources = GWT.create(LeaderboardTableResources.class); + + private ImageResource pauseIcon; + private ImageResource playIcon; + + private final BusyIndicator busyIndicator; + + /** + * Tells whether the leaderboard settings were explicitly changed by an external call to + * {@link #updateSettings(LeaderboardSettings)}. If so, a {@link #playStateChanged(PlayStates, PlayModes) play state + * change} will not automatically lead to a settings change. + */ + private boolean settingsUpdatedExplicitly = false; + + /** + * Tells if the leaderboard is currently handling a {@link #playStateChanged(PlayStates, PlayModes) play state + * change}. If this is the case, a call to {@link #updateSettings(LeaderboardSettings)} won't set the + * {@link #settingsUpdatedExplicitly} flag. + */ + private boolean currentlyHandlingPlayStateChange; + + private PlayModes oldPlayMode; + + 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; + + private boolean initialCompetitorFilterHasBeenApplied = false; + private final boolean showCompetitorFilterStatus; + + private CompetitorFilterPanel competitorFilterPanel; + + protected StringMessages getStringMessages() { + return stringMessages; + } + + @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 VerticalPanel getContentPanel() { + return contentPanel; + } + + protected ImageResource getSettingsIcon() { + return componentResources.settingsIcon(); + } + + public void updateSettings(final LeaderboardSettings newSettings) { + boolean oldShallAddOverallDetails = shallAddOverallDetails(); + if (newSettings.getOverallDetailsToShow() != null) { + selectedOverallDetailColumns.clear(); + selectedOverallDetailColumns.addAll(newSettings.getOverallDetailsToShow()); + } + if (!newSettings.isUpdateUponPlayStateChange() || !currentlyHandlingPlayStateChange) { + settingsUpdatedExplicitly = true; + } + 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.toggleExpansion(); + 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()); + } + // 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; + } + + final boolean oldBusyState = getBusyIndicator().isBusy(); + getBusyIndicator().setBusy(true); + Runnable doWhenNecessaryDetailHasBeenLoaded = new Runnable() { + @Override + public void run() { + setAutoExpandPreSelectedRace(false); // avoid expansion during updateLeaderboard(...); will expand later + // if it was expanded before + // update leaderboard after settings panel column selection change + updateLeaderboard(leaderboard); + setAutoExpandPreSelectedRace(newSettings.isAutoExpandPreSelectedRace()); + + if (newSettings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { + timer.setRefreshInterval(newSettings.getDelayBetweenAutoAdvancesInMilliseconds()); + } + for (ExpandableSortableColumn expandableSortableColumn : columnsToExpandAgain) { + expandableSortableColumn.toggleExpansion(); + } + if (newSettings.getNameOfRaceToSort() != null) { + final RaceColumn raceColumnByRaceName = getRaceColumnByRaceName(newSettings + .getNameOfRaceToSort()); + if (raceColumnByRaceName != null) { + getLeaderboardTable().sortColumn(raceColumnByRaceName, /* ascending */true); + } + } + getBusyIndicator().setBusy(oldBusyState); + } + }; + 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 */getNamesOfExpandedRaces(), + shallAddOverallDetails(), previousLeaderboard.getId(), /* fillNetPointsUncorrected */ 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; + } + + 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; + } + + @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.totalPoints != null) { + addedScores += entryBefore.totalPoints; + } + // else "add 0" + if (raceColumn.getName().equals(getRaceColumnName())) { + break; // we've reached the current column - stop here + } + } + return addedScores; + } + + /** + * Displays a combination of total points and maxPointsReason in bold, transparent, strike-through, depending on + * various criteria. Here's how: + * + *
    +         *                                  total 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 totalOrAddedPointsAsText = isShowAddedScores() ? addedScores + : entry.totalPoints == null ? "" : scoreFormat.format(entry.totalPoints); + String netOrAddedPointsAsText = isShowAddedScores() ? addedScores + : entry.netPoints == null ? "" : scoreFormat.format(entry.netPoints); + 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(totalOrAddedPointsAsText); + html.appendHtmlConstant(""); + } else { + html.appendHtmlConstant(" "); + html.appendHtmlConstant(netOrAddedPointsAsText); + 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_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_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, + DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL}; + } + + 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 totalPoints = object.fieldsByRaceColumnName.get(getRaceColumnName()).totalPoints; + return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); + } + + @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 CurrentSpeedOverGroundInKnots(), + 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) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.legDetails != null) { + double distanceTraveledInMeters = 0; + long timeInMilliseconds = 0; + for (LegEntryDTO legDetail : fieldsForRace.legDetails) { + if (legDetail != null) { + if (legDetail.distanceTraveledInMeters != null && legDetail.timeInMilliseconds != null) { + distanceTraveledInMeters += legDetail.distanceTraveledInMeters; + timeInMilliseconds += legDetail.timeInMilliseconds; + } else { + distanceTraveledInMeters = 0; + timeInMilliseconds = 0; + break; + } + } + } + if (timeInMilliseconds != 0) { + result = distanceTraveledInMeters / (double) timeInMilliseconds * 1000 * 3600 / 1852; + } + } + 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) { + 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.distanceTraveledInMeters != null) { + if (result == null) { + result = 0.0; + } + result += legDetail.distanceTraveledInMeters; + } + } + } + } + return result; + } + } + + 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 class CurrentSpeedOverGroundInKnots implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double 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) { + result = lastLegDetail.currentSpeedOverGroundInKnots; + } + } + } + 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.totalPoints != 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 totals for a competitor for the entire leaderboard. + * + * @author Axel Uhl (D043530) + * + */ + private class TotalsColumn extends LeaderboardSortableColumnWithMinMax { + private final String columnStyle; + + protected TotalsColumn(String columnStyle) { + super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); + this.columnStyle = columnStyle; + setHorizontalAlignment(ALIGN_CENTER); + } + + @Override + public String getValue(LeaderboardRowDTO object) { + Double totalPoints = object.totalPoints; + return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); + } + + @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.totalsColumnTooltip()); + } + } + + 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(tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()); + 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); + if (row != null) { + redrawRow(row); + } + } + + private void redrawRow(final LeaderboardRowDTO row) { + final List leaderboardDataList = getData().getList(); + synchronized (leaderboardDataList) { + redrawRow(row, leaderboardDataList); + } + } + + /** + * Ensure that the checkbox is redrawn when the competitor selection changes + */ + @Override + public void removedFromSelection(CompetitorDTO competitor) { + final LeaderboardRowDTO row = getRow(competitor); + if (row != null) { + redrawRow(row); + } + } + + @Override + public void updateMinMax() {} + + @Override + protected ListDataProvider getListDataProvider() { + return getData(); + } + } + + 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(); + } + } + + public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + LeaderboardSettings settings, CompetitorSelectionProvider competitorSelectionProvider, + String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, + final UserAgentDetails userAgent, boolean showRaceDetails) { + this(sailingService, asyncActionsExecutor, settings, false, /* preSelectedRace */null, competitorSelectionProvider, + null, leaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails); + } + + public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, + CompetitorSelectionProvider competitorSelectionProvider, String leaderboardGroupName, + String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, + final UserAgentDetails userAgent, boolean showRaceDetails) { + this(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 */3000l), leaderboardGroupName, + leaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails, + /* competitorSearchTextBox */ null, /* showSelectionCheckbox */ true, /* optionalRaceTimesInfoProvider */ null, + /* autoExpandLastRaceColumn */ false, /* adjustTimerDelay */ true, /*autoApplyTop30Filter*/ false, false); + } + + public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, + CompetitorSelectionProvider competitorSelectionProvider, Timer timer, String leaderboardGroupName, + String leaderboardName, final ErrorReporter errorReporter, final StringMessages stringMessages, + final UserAgentDetails userAgent, boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, + boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, + boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, boolean showCompetitorFilterStatus) { + this.setTitle(stringMessages.leaderboard()); + 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.selectedLegDetails = new ArrayList(); + this.selectedRaceDetails = new ArrayList(); + this.selectedOverallDetailColumns = new ArrayList(); + this.raceTimesInfoProvider = optionalRaceTimesInfoProvider; + this.selectedManeuverDetails = new ArrayList(); + this.adjustTimerDelay = adjustTimerDelay; + this.initialCompetitorFilterHasBeenApplied = !autoApplyTopNFilter; + this.showCompetitorFilterStatus = showCompetitorFilterStatus; + overallDetailColumnMap = createOverallDetailColumnMap(); + settingsUpdatedExplicitly = !settings.isUpdateUponPlayStateChange(); + this.leaderboardUpdateListener = new ArrayList(); + if (settings.getLegDetailsToShow() != null) { + selectedLegDetails.addAll(settings.getLegDetailsToShow()); + } + if (settings.getManeuverDetailsToShow() != null) { + selectedManeuverDetails.addAll(settings.getManeuverDetailsToShow()); + } + if (settings.getRaceDetailsToShow() != null) { + selectedRaceDetails.addAll(settings.getRaceDetailsToShow()); + } + if (settings.getOverallDetailsToShow() != null) { + selectedOverallDetailColumns.addAll(settings.getOverallDetailsToShow()); + } + setAutoExpandPreSelectedRace(settings.isAutoExpandPreSelectedRace()); + this.autoExpandLastRaceColumn = autoExpandLastRaceColumn; + if (settings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { + timer.setRefreshInterval(settings.getDelayBetweenAutoAdvancesInMilliseconds()); + } + this.timer = timer; + 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(); + selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider); + RACE_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableRaceColumnHeader(); + LEG_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegColumnHeader(); + LEG_DETAIL_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumnHeader(); + RACE_COLUMN_STYLE = tableResources.cellTableStyle().cellTableRaceColumn(); + LEG_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegColumn(); + LEG_DETAIL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumn(); + TOTAL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableTotalColumn(); + leaderboardTable = new SortedCellTableWithStylableHeaders( + /* pageSize */10000, tableResources); + 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"); + getLeaderboardTable().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()); + setShowAddedScores(settings.isShowAddedScores()); + setShowCompetitorSailId(settings.isShowCompetitorSailIdColumn()); + setShowCompetitorFullName(settings.isShowCompetitorFullNameColumn()); + setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(settings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()); + if (timer.isInitialized()) { + loadCompleteLeaderboard(getLeaderboardDisplayDate()); + } + + contentPanel = new VerticalPanel(); + leaderboardTable.getElement().getStyle().setMarginTop(10, Unit.PX); + contentPanel.setStyleName(STYLE_LEADERBOARD_CONTENT); + busyIndicator = new SimpleBusyIndicator(false, 0.8f); + busyIndicator.ensureDebugId("BusyIndicator"); + + + // the information panel + if (!isEmbedded) { + Widget toolbarPanel = createToolbarPanel(); + contentPanel.add(toolbarPanel); + } + if (competitorSearchTextBox != null) { + contentPanel.add(competitorSearchTextBox); + contentPanel.add(busyIndicator); + competitorSearchTextBox.getSettingsButton().addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + new SettingsDialog(LeaderboardPanel.this, stringMessages).show(); + } + }); + this.competitorFilterPanel = competitorSearchTextBox; + } + SortedCellTable leaderboardTable = getLeaderboardTable(); + // leaderboardTable.getElement().getStyle().setMarginTop(5, Unit.PX); + filterControlPanel = new HorizontalPanel(); + filterControlPanel.setStyleName("LeaderboardPanel-FilterControl-Panel"); + contentPanel.add(leaderboardTable); + if (showCompetitorFilterStatus) { + contentPanel.add(createFilterDeselectionControl()); + } + setWidget(contentPanel); + raceNameForDefaultSorting = settings.getNameOfRaceToSort(); + } + + 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; + } + + 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) { + getBusyIndicator().setBusy(true); + competitorFilterPanel.clearAllActiveFilters(); + timeChanged(new Date(), null); + } + }); + 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.toggleExpansion(); + // 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); + } + } + + private void loadCompleteLeaderboard(Date date) { + if (needsDataLoading()) { + GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction(sailingService, + getLeaderboardName(), useNullAsTimePoint() ? null : date, + /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(), shallAddOverallDetails(), /* previousLeaderboard */ + getLeaderboard(), isFillNetPointsUncorrected(), timer, errorReporter, stringMessages); + this.asyncActionsExecutor.execute(getLeaderboardByNameAction, LOAD_LEADERBOARD_DATA_CATEGORY, + new AsyncCallback() { + @Override + public void onSuccess(LeaderboardDTO result) { + updateLeaderboard(result); + getBusyIndicator().setBusy(false); + } + + @Override + public void onFailure(Throwable caught) { + getBusyIndicator().setBusy(false); + getErrorReporter() + .reportError("Error trying to obtain leaderboard contents: " + caught.getMessage(), + true /* silentMode */); + } + }); + } else { + getBusyIndicator().setBusy(false); + } + } + + protected boolean isFillNetPointsUncorrected() { + return false; + } + + /** + * 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; + } + + private boolean needsDataLoading() { + return isVisible(); + } + + /** + * Determine from column expansion state which races need their leg details + */ + private Collection getNamesOfExpandedRaces() { + Collection namesOfExpandedRaces = 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()) { + namesOfExpandedRaces.add(raceColumn.getRaceColumnName()); + } + } + } + return namesOfExpandedRaces; + } + + 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. + */ + protected void updateLeaderboard(LeaderboardDTO leaderboard) { + if (leaderboard != null) { + Collection> columnsToCollapseAndExpandAgain = getExpandedRaceColumnsWhoseDisplayedLegCountChanged(leaderboard); + for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { + columnToCollapseAndExpandAgain.toggleExpansion(); + } + 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.toggleExpansion(); + } + 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.toggleExpansion(); + 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(""); + } + + List> liveRaces = leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()); + boolean hasLiveRace = !liveRaces.isEmpty(); + liveRaceLabel.setText(hasLiveRace ? getLiveRacesText() : ""); + if (hasLiveRace) { + String liveRaceText = ""; + if(liveRaces.size() == 1) { + com.sap.sse.common.Util.Pair liveRace = liveRaces.get(0); + liveRaceText = stringMessages.raceIsLive("'" + liveRace.getA().getRaceColumnName() + "'"); + } else { + String raceNames = ""; + for (com.sap.sse.common.Util.Pair liveRace : liveRaces) { + raceNames += "'" + liveRace.getA().getRaceColumnName() + "', "; + } + // remove last ", " + raceNames = raceNames.substring(0, raceNames.length() - 2); + liveRaceText = stringMessages.racesAreLive(raceNames); + } + liveRaceLabel.setText(liveRaceText); + } else { + liveRaceLabel.setText(""); + } + scoreCorrectionLastUpdateTimeLabel.setVisible(!hasLiveRace); + liveRaceLabel.setVisible(hasLiveRace); + } + informLeaderboardUpdateListenersAboutLeaderboardUpdated(leaderboard); + getBusyIndicator().setBusy(false); + } + } + + /** + * 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 + */ + private Iterable getCompetitors(RaceIdentifier race) { + Set result = new HashSet(); + 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 (iraceColumn + */ + private void correctColumnData(RaceColumn raceColumn, RaceColumnDTO race) { + int columnIndex = getRaceColumnPosition(raceColumn); + if (raceColumn.isExpansionEnabled() != race.hasTrackedRaces() || race.isMedalRace() != raceColumn.isMedalRace()) { + if (raceColumn.isExpanded()) { + raceColumn.toggleExpansion(); // 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 TotalsColumn) { + 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, getStringMessages(), + 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 TotalsColumn)) { + addColumn(new TotalsColumn(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; + } + + protected 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(getLeaderboardDisplayDate()); + } + + @Override + public void playStateChanged(PlayStates playState, PlayModes playMode) { + currentlyHandlingPlayStateChange = true; + + if(!isEmbedded) { + playPause.setHTML(getPlayPauseImgHtml(playState)); + playPause.setTitle(playState == PlayStates.Playing ? stringMessages.pauseAutomaticRefresh() : stringMessages.autoRefresh()); + } + if (!settingsUpdatedExplicitly && playMode != oldPlayMode) { + // if settings weren't explicitly modified, auto-switch to live mode settings and sort for + // any pre-selected race; we need to copy the previously selected race columns to the new RaceColumnSelection + updateSettings(LeaderboardSettingsFactory.getInstance().createNewSettingsForPlayMode( + playMode, + /* don't touch columnToSort if no race was pre-selected */ preSelectedRace == null ? null + : preSelectedRace.getRaceName(), + /* don't change nameOfRaceColumnToShow */null, + /* set nameOfRaceToShow if race was pre-selected */preSelectedRace == null ? null : preSelectedRace + .getRaceName(), getRaceColumnSelection(), /* leave showRegattaRank and overall details unchanged */ null, + /* take into account state of competitor columns*/isShowCompetitorSailId(), isShowCompetitorFullName())); + } + currentlyHandlingPlayStateChange = false; + oldPlayMode = playMode; + } + + @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(Collections.unmodifiableList(selectedManeuverDetails), + Collections.unmodifiableList(selectedLegDetails), Collections.unmodifiableList(selectedRaceDetails), + Collections.unmodifiableList(selectedOverallDetailColumns), /* All races to select */ leaderboard.getRaceList(), + raceColumnSelection.getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard), raceColumnSelection, autoExpandPreSelectedRace, + isShowAddedScores(), timer.getRefreshInterval(), isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), + isShowCompetitorSailId(), isShowCompetitorFullName(), stringMessages); + } + + @Override + public String getLocalizedShortName() { + return stringMessages.leaderboard(); + } + + private LeaderboardRowDTO getRow(CompetitorDTO competitor) { + synchronized (getData().getList()) { + for (LeaderboardRowDTO row : getData().getList()) { + if (row.competitor.equals(competitor)) { + return row; + } + } + } + return null; + } + + @Override + public void addedToSelection(CompetitorDTO competitor) { + LeaderboardRowDTO row = getRow(competitor); + if (row != null) { + leaderboardSelectionModel.setSelected(row, true); + } + } + + @Override + public void removedFromSelection(CompetitorDTO competitor) { + LeaderboardRowDTO row = getRow(competitor); + if (row != null) { + leaderboardSelectionModel.setSelected(row, false); + } + } + + @Override + public BusyIndicator getBusyIndicator() { + return busyIndicator; + } + + @Override + public boolean hasBusyIndicator() { + return true; + } + + 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); + } + } + + public void addLeaderboardUpdateListener(LeaderboardUpdateListener listener) { + this.leaderboardUpdateListener.add(listener); + } + + protected void informLeaderboardUpdateListenersAboutLeaderboardUpdated(LeaderboardDTO leaderboard) { + for (LeaderboardUpdateListener listener : 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()); + if (!liveRaces.isEmpty()) { + if(liveRaces.size() == 1) { + com.sap.sse.common.Util.Pair liveRace = liveRaces.get(0); + result = stringMessages.raceIsLive("'" + liveRace.getA().getRaceColumnName() + "'"); + } else { + String raceNames = ""; + for (com.sap.sse.common.Util.Pair liveRace : liveRaces) { + raceNames += "'" + liveRace.getA().getRaceColumnName() + "', "; + } + // remove last ", " + raceNames = raceNames.substring(0, raceNames.length() - 2); + result = stringMessages.racesAreLive(raceNames); + } + } + return result; + } + + @Override + public String getDependentCssClassName() { + return "leaderboard"; + } +} From ab2ce26bda13cf34c53bc8c1c198ca641700ea7e Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Thu, 26 Nov 2015 11:30:21 +0100 Subject: [PATCH 048/306] Adjusted RaceBoard.css to avoid wrapping in series and fleet label --- java/com.sap.sailing.gwt.ui/RaceBoard.css | 1 + 1 file changed, 1 insertion(+) diff --git a/java/com.sap.sailing.gwt.ui/RaceBoard.css b/java/com.sap.sailing.gwt.ui/RaceBoard.css index 4b9d0f25dae..0175c2a97dd 100644 --- a/java/com.sap.sailing.gwt.ui/RaceBoard.css +++ b/java/com.sap.sailing.gwt.ui/RaceBoard.css @@ -692,6 +692,7 @@ img.openColumn { .compactHeader .RegattaRaceInformation-Header .RaceSeriesAndFleet-Label { display: block; margin-top: 5px; + white-space: nowrap; } .compactHeader .RegattaAndRaceTime-Header { top: 11px; From 84b744c7d06c8a4ce38f7cd408d8c2ab5d51c7e1 Mon Sep 17 00:00:00 2001 From: fmittag Date: Thu, 26 Nov 2015 14:07:19 +0100 Subject: [PATCH 049/306] improved the way the UI informed about the change of the selected leaderboard in a multileaderobard --- .../multileaderboard/OldMultiLeaderboard.java | 17 ++++--- .../ui/leaderboard/MultiLeaderboardPanel.java | 49 ++++++++++++++----- .../SelectedLeaderboardChangeListener.java | 14 ++++++ .../SelectedLeaderboardChangeProvider.java | 11 +++++ 4 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/SelectedLeaderboardChangeListener.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/SelectedLeaderboardChangeProvider.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java index c5319b1ea92..f73f6ff4f21 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java @@ -28,6 +28,8 @@ import com.sap.sailing.gwt.home.desktop.partials.old.LeaderboardDelegate; import com.sap.sailing.gwt.ui.client.DebugIdHelper; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; +import com.sap.sailing.gwt.ui.leaderboard.SelectedLeaderboardChangeListener; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; import com.sap.sailing.gwt.ui.leaderboard.MultiLeaderboardPanel; import com.sap.sailing.gwt.ui.leaderboard.ScoringSchemeTypeFormatter; @@ -36,7 +38,7 @@ 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.SettingsDialog; -public class OldMultiLeaderboard extends Composite { +public class OldMultiLeaderboard extends Composite implements SelectedLeaderboardChangeListener { private static OldMultiLeaderboardUiBinder uiBinder = GWT.create(OldMultiLeaderboardUiBinder.class); interface OldMultiLeaderboardUiBinder extends UiBinder { @@ -169,16 +171,12 @@ public class OldMultiLeaderboard extends Composite { public void setMultiLeaderboard(MultiLeaderboardPanel multiLeaderboardPanel, final Timer timer) { this.autoRefreshTimer = timer; this.multiLeaderboardPanel = multiLeaderboardPanel; + this.multiLeaderboardPanel.addSelectedLeaderboardChangeListener(this); oldMultiLeaderboardPanel.add(multiLeaderboardPanel); } public void updatedMultiLeaderboard(LeaderboardDTO leaderboard, boolean hasLiveRace) { - if(multiLeaderboardPanel.getSelectedLeaderboardPanel() != null) { - busyIndicatorDiv.removeAllChildren(); - busyIndicatorDiv.appendChild(multiLeaderboardPanel.getSelectedLeaderboardPanel().getBusyIndicator().getElement()); - busyIndicatorDiv.appendChild(emptyDivElement); - } if(leaderboard != null) { String comment = leaderboard.getComment() != null ? leaderboard.getComment() : ""; String scoringScheme = leaderboard.scoringScheme != null ? ScoringSchemeTypeFormatter.getDescription(leaderboard.scoringScheme, StringMessages.INSTANCE) : ""; @@ -209,6 +207,13 @@ public class OldMultiLeaderboard extends Composite { } } } + + @Override + public void onSelectedLeaderboardChanged(LeaderboardPanel selectedLeaderboard) { + busyIndicatorDiv.removeAllChildren(); + busyIndicatorDiv.appendChild(selectedLeaderboard.getBusyIndicator().getElement()); + busyIndicatorDiv.appendChild(emptyDivElement); + } public interface OldMultiLeaderboardDelegate extends LeaderboardDelegate{ } 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 6030ff94216..afb14fae51b 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 @@ -3,8 +3,10 @@ package com.sap.sailing.gwt.ui.leaderboard; import java.util.ArrayList; 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 com.google.gwt.core.shared.GWT; import com.google.gwt.dom.client.Style.FontWeight; @@ -35,7 +37,7 @@ import com.sap.sse.gwt.client.useragent.UserAgentDetails; * A panel managing multiple {@link LeaderboardPanel}s (e.g. from a meta leaderboard) so that the user can switch between them. * @author Frank */ -public class MultiLeaderboardPanel extends AbstractLazyComponent implements TimeListener { +public class MultiLeaderboardPanel extends AbstractLazyComponent implements TimeListener, SelectedLeaderboardChangeProvider { private LeaderboardPanel selectedLeaderboardPanel; private FlowPanel selectedLeaderboardFlowPanel; @@ -60,7 +62,8 @@ public class MultiLeaderboardPanel extends AbstractLazyComponent leaderboardUpdateListener; + private final Set leaderboardUpdateListeners; + private final Set selectedLeaderboardChangeListeners; public MultiLeaderboardPanel(SailingServiceAsync sailingService, String metaLeaderboardName, AsyncActionsExecutor asyncActionsExecutor, Timer timer, boolean isEmbedded, String preselectedLeaderboardName, RaceIdentifier preselectedRace, @@ -81,7 +84,8 @@ public class MultiLeaderboardPanel extends AbstractLazyComponent>(); leaderboardNamesAndSettings = new HashMap(); - leaderboardUpdateListener = new ArrayList(); + leaderboardUpdateListeners = new HashSet(); + selectedLeaderboardChangeListeners = new HashSet(); } private LeaderboardSettings getOrCreateLeaderboardSettings(String leaderboardName, LeaderboardSettings currentLeaderboardSettings) { @@ -152,7 +156,7 @@ public class MultiLeaderboardPanel extends AbstractLazyComponent> newLeaderboardNamesAndDisplayNames) { @@ -177,10 +181,10 @@ public class MultiLeaderboardPanel extends AbstractLazyComponentnull LeaderboardPanel + */ + void onSelectedLeaderboardChanged(LeaderboardPanel selectedLeaderboard); + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/SelectedLeaderboardChangeProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/SelectedLeaderboardChangeProvider.java new file mode 100644 index 00000000000..e19f52bd4bb --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/SelectedLeaderboardChangeProvider.java @@ -0,0 +1,11 @@ +package com.sap.sailing.gwt.ui.leaderboard; + + +public interface SelectedLeaderboardChangeProvider { + + void addSelectedLeaderboardChangeListener(SelectedLeaderboardChangeListener listener); + + void removeSelectedLeaderboardChangeListener(SelectedLeaderboardChangeListener listener); + + void setSelectedLeaderboard(LeaderboardPanel selectedLeaderboard); +} From 99b712eccb3e1db857baab0fdc33fef9fdb83f2b Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Thu, 26 Nov 2015 16:35:27 +0100 Subject: [PATCH 050/306] revised AbstractTrackedRacesListComposite.java --- .../AbstractLeaderboardConfigPanel.java | 12 +- .../AbstractTrackedRacesListComposite.java | 112 ++++++++++-------- .../adminconsole/CompetitorTableWrapper.java | 2 +- .../ControlPointTableWrapper.java | 2 +- .../DeviceIdentifierTableWrapper.java | 2 +- .../DeviceMappingTableWrapper.java | 2 +- .../gwt/ui/adminconsole/MarkTableWrapper.java | 2 +- .../gwt/ui/adminconsole/RaceTableWrapper.java | 7 +- .../gwt/ui/adminconsole/TableWrapper.java | 12 +- ...ileImportDeviceIdentifierTableWrapper.java | 2 +- .../TrackedRacesListComposite.java | 2 +- .../ui/adminconsole/WaypointTableWrapper.java | 2 +- 12 files changed, 91 insertions(+), 68 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index 7e0352c989d..4ad8007dbc1 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -30,6 +30,7 @@ import com.sap.sailing.domain.common.RegattaNameAndRaceName; import com.sap.sailing.domain.common.dto.FleetDTO; import com.sap.sailing.domain.common.dto.NamedDTO; import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceDTO; import com.sap.sailing.gwt.ui.adminconsole.RaceColumnInLeaderboardDialog.RaceColumnDescriptor; import com.sap.sailing.gwt.ui.client.LeaderboardsDisplayer; import com.sap.sailing.gwt.ui.client.LeaderboardsRefresher; @@ -51,6 +52,8 @@ 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.celltable.RefreshableMultiSelectionModel; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -71,7 +74,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement protected final CellTable leaderboardTable; protected final RaceTableWrapper> raceColumnTable; - protected final SetSelectionModel raceColumnTableSelectionModel; + protected final RefreshableSelectionModel raceColumnTableSelectionModel; protected RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceInLeaderboard; @@ -85,7 +88,8 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement protected final RefreshableMultiSelectionModel refreshableLeaderboardSelectionModel; - protected final RaceSelectionProvider raceSelectionProvider; + //protected final RaceSelectionProvider raceSelectionProvider; + protected final RefreshableSingleSelectionModel raceSingelSelectionModel; private final LeaderboardsRefresher leaderboardsRefresher; @@ -210,7 +214,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement trackedRacesCaptionPanel.setContentWidget(trackedRacesPanel); trackedRacesCaptionPanel.setStyleName("bold"); - raceSelectionProvider = new RaceSelectionModel(); + //raceSelectionProvider = new RaceSelectionModel(); trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher, raceSelectionProvider, stringMessages, /* multiselection */false, isActionButtonsEnabled()); trackedRacesListComposite.ensureDebugId("TrackedRacesListComposite"); @@ -239,7 +243,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement sailingService, stringMessages, errorReporter, multiSelection); raceColumnTable.getTable().setWidth("100%"); addColumnsToRacesTable(raceColumnTable.getTable()); - this.raceColumnTableSelectionModel = raceColumnTable.getSelectionModel(); + this.raceColumnTableSelectionModel = raceColumnTable.getRefreshableSelectionModel(); raceColumnTableSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { public void onSelectionChange(SelectionChangeEvent event) { leaderboardRaceColumnSelectionChanged(); 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 fe16fdeda07..1acaf12b83c 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,9 +1,9 @@ package com.sap.sailing.gwt.ui.adminconsole; import java.util.ArrayList; -import java.util.Collections; 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; @@ -27,13 +27,10 @@ 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.SelectionModel; -import com.google.gwt.view.client.SingleSelectionModel; 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.RaceSelectionChangeListener; import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.RegattasDisplayer; @@ -46,20 +43,23 @@ import com.sap.sse.common.Util; 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.LabeledAbstractFilterablePanel; import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; public abstract class AbstractTrackedRacesListComposite extends SimplePanel implements Component, - RegattasDisplayer, RaceSelectionChangeListener { + RegattasDisplayer/*, RaceSelectionChangeListener*/ { protected final long DEFAULT_LIVE_DELAY_IN_MILLISECONDS = 5000; private final boolean multiSelection; - private boolean dontFireNextSelectionChangeEvent; + //private boolean dontFireNextSelectionChangeEvent; - private SelectionModel selectionModel; + protected RefreshableSelectionModel refreshableSelectionModel; private SelectionCheckboxColumn selectionCheckboxColumn; @@ -74,7 +74,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl protected final SailingServiceAsync sailingService; protected final ErrorReporter errorReporter; protected final RegattaRefresher regattaRefresher; - private final RaceSelectionProvider raceSelectionProvider; + //private final RaceSelectionProvider raceSelectionProvider; protected final StringMessages stringMessages; private Button btnRefresh; @@ -97,24 +97,40 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl this.errorReporter = errorReporter; this.regattaRefresher = regattaRefresher; this.multiSelection = hasMultiSelection; - this.raceSelectionProvider = raceSelectionProvider; +// this.raceSelectionProvider = raceSelectionProvider; this.stringMessages = stringMessages; } protected void createUI() { AdminConsoleTableResources tableResources = GWT.create(AdminConsoleTableResources.class); - this.selectionCheckboxColumn = new SelectionCheckboxColumn(tableResources.cellTableStyle().cellTableCheckboxSelected(), - tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { - @Override - protected ListDataProvider getListDataProvider() { - return raceList; - } + if (multiSelection) { + this.selectionCheckboxColumn = new SelectionCheckboxColumn( + tableResources.cellTableStyle().cellTableCheckboxSelected(), + tableResources.cellTableStyle().cellTableCheckboxDeselected(), + tableResources.cellTableStyle().cellTableCheckboxColumnCell(), + new EntityIdentityComparator() { - @Override - public Boolean getValue(RaceDTO row) { - return raceTable.getSelectionModel().isSelected(row); - } - }; + @Override + public boolean representSameEntity(RaceDTO dto1, RaceDTO dto2) { + return dto1.getRaceIdentifier().equals(dto2.getRaceIdentifier()); + } + }) { + @Override + protected ListDataProvider getListDataProvider() { + return raceList; + } + + @Override + public Boolean getValue(RaceDTO row) { + return raceTable.getSelectionModel().isSelected(row); + } + }; + refreshableSelectionModel = selectionCheckboxColumn.getSelectionModel(); + raceTable.setSelectionModel(refreshableSelectionModel, this.selectionCheckboxColumn.getSelectionManager()); + } else { + refreshableSelectionModel = new RefreshableSingleSelectionModel(); + raceTable.setSelectionModel(refreshableSelectionModel); + } raceList = new ListDataProvider(); settings = new TrackedRacesSettings(); @@ -139,13 +155,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl raceTable.ensureDebugId("TrackedRacesCellTable"); ListHandler columnSortHandler = setupTableColumns(stringMessages); raceTable.setWidth("300px"); - if (multiSelection) { - selectionModel = this.selectionCheckboxColumn.getSelectionModel(); - raceTable.setSelectionModel(selectionModel, this.selectionCheckboxColumn.getSelectionManager()); - } else { - selectionModel = new SingleSelectionModel(); - raceTable.setSelectionModel(selectionModel); - } + raceTable.setVisible(false); panel.add(raceTable); raceList.addDataDisplay(raceTable); @@ -153,18 +163,20 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl raceTable.getSelectionModel().addSelectionChangeHandler(new Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { - List selectedRaces = getSelectedRaces(); + Set selectedRaces = refreshableSelectionModel.getSelectedSet(); makeControlsReactToSelectionChange(selectedRaces); - if (dontFireNextSelectionChangeEvent) { + /*if (dontFireNextSelectionChangeEvent) { dontFireNextSelectionChangeEvent = false; } else { + List selectedRaceIdentifiers = new ArrayList(); for (RaceDTO selectedRace : selectedRaces) { selectedRaceIdentifiers.add(selectedRace.getRaceIdentifier()); } AbstractTrackedRacesListComposite.this.raceSelectionProvider.setSelection(selectedRaceIdentifiers, AbstractTrackedRacesListComposite.this); - } + + }*/ } }); filterablePanelRaces = new LabeledAbstractFilterablePanel(lblFilterRaces, allRaces, raceTable, raceList) { @@ -196,7 +208,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl addControlButtons(trackedRacesButtonPanel); } - abstract protected void makeControlsReactToSelectionChange(List selectedRaces); + abstract protected void makeControlsReactToSelectionChange(Set selectedRaces); abstract protected void makeControlsReactToFillRegattas(Iterable regattas); @@ -381,14 +393,14 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl return columnSortHandler; } - + /* @Override public void onRaceSelectionChange(List selectedRaces) { for (RaceDTO raceFromAllRaces : raceList.getList()) { selectionModel.setSelected(raceFromAllRaces, selectedRaces.contains(raceFromAllRaces.getRaceIdentifier())); } } - +*/ @Override public boolean hasSettings() { return true; @@ -405,7 +417,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl // set the new delay to all selected races List raceIdentifiersToUpdate = new ArrayList(); - for (RaceDTO raceDTO : getSelectedRaces()) { + for (RaceDTO raceDTO : refreshableSelectionModel.getSelectedSet()) { raceIdentifiersToUpdate.add(raceDTO.getRaceIdentifier()); } @@ -439,8 +451,8 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl return this; } - public void addRaceSelectionChangeListener(RaceSelectionChangeListener listener) { - this.raceSelectionProvider.addRaceSelectionChangeListener(listener); + public void addRaceSelectionChangeHandler(Handler handler) { + refreshableSelectionModel.addSelectionChangeHandler(handler); } public RaceDTO getRaceByIdentifier(RaceIdentifier raceIdentifier) { @@ -455,7 +467,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl } return result; } - +/* List getSelectedRaces() { List result = new ArrayList(); if (raceList != null) { @@ -467,15 +479,15 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl } 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())) { - dontFireNextSelectionChangeEvent = true; - selectionModel.setSelected(race, true); + //dontFireNextSelectionChangeEvent = true; + refreshableSelectionModel.setSelected(race, true); break; } } @@ -483,13 +495,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl } public void clearSelection() { - List emptySelection = Collections.emptyList(); - raceSelectionProvider.setSelection(emptySelection, /* listenersNotToNotify */this); - if (raceList != null) { - for (RaceDTO race : raceList.getList()) { - selectionModel.setSelected(race, false); - } - } + refreshableSelectionModel.clear(); } @Override @@ -504,22 +510,23 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl } List newAllRaces = new ArrayList(); List newAllRegattas = new ArrayList(); - List newAllRaceIdentifiers = new ArrayList(); + //List newAllRaceIdentifiers = new ArrayList(); for (RegattaDTO regatta : regattas) { newAllRegattas.add(regatta); for (RaceDTO race : regatta.races) { if (race != null) { if (raceIsToBeAddedToList(race)) { newAllRaces.add(race); - newAllRaceIdentifiers.add(race.getRaceIdentifier()); + //newAllRaceIdentifiers.add(race.getRaceIdentifier()); } } } } allRaces = newAllRaces; filterablePanelRaces.updateAll(allRaces); - raceSelectionProvider.setAllRaces(newAllRaceIdentifiers); // have this object be notified; triggers + //raceSelectionProvider.setAllRaces(newAllRaceIdentifiers); // have this object be notified; triggers // onRaceSelectionChange + refreshableSelectionModel.refreshSelectionModel(allRaces); } /** @@ -532,5 +539,8 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl 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/CompetitorTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java index 9b74a2a56cb..b27f1894845 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java @@ -31,7 +31,7 @@ public class CompetitorTableWrapper> ext public CompetitorTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages,ErrorReporter errorReporter, boolean multiSelection, boolean enablePager) { - super(sailingService, stringMessages, errorReporter, multiSelection, enablePager); + super(sailingService, stringMessages, errorReporter, multiSelection, enablePager, null /*EntityIdentityComparator for RefreshableSelectionModel*/); ListHandler competitorColumnListHandler = getColumnSortHandler(); // competitors table diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java index 3671f8d60f4..d08c9492800 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java @@ -12,7 +12,7 @@ import com.sap.sse.gwt.client.ErrorReporter; public class ControlPointTableWrapper> extends TableWrapper { public ControlPointTableWrapper(boolean multiSelection, SailingServiceAsync sailingService, final StringMessages stringMessages, ErrorReporter errorReporter) { - super(sailingService, stringMessages, errorReporter, multiSelection, /* enablePager */ true); + super(sailingService, stringMessages, errorReporter, multiSelection, /* enablePager */ true, null /*EntityIdentityComparator for RefreshableSelectionModel*/); TextColumn nameColumn = new TextColumn() { @Override public String getValue(ControlPointDTO d) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceIdentifierTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceIdentifierTableWrapper.java index 7024f8a3d6a..a47a1923e11 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceIdentifierTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceIdentifierTableWrapper.java @@ -11,7 +11,7 @@ public class DeviceIdentifierTableWrapper extends TableWrapper typeColumn = new TextColumn() { @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceMappingTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceMappingTableWrapper.java index 339a10e99a1..66d654661f3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceMappingTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceMappingTableWrapper.java @@ -30,7 +30,7 @@ public class DeviceMappingTableWrapper extends TableWrapper() { @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java index b57c4937573..6dff4737162 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java @@ -19,7 +19,7 @@ import com.sap.sse.gwt.client.ErrorReporter; public class MarkTableWrapper> extends TableWrapper { public MarkTableWrapper(boolean multiSelection, SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter errorReporter) { - super(sailingService, stringMessages, errorReporter, multiSelection, true); + super(sailingService, stringMessages, errorReporter, multiSelection, true, null /*EntityIdentityComparator for RefreshableSelectionModel*/); TextColumn markNameColumn = new TextColumn() { @Override public String getValue(MarkDTO markDTO) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java index 5d1eb5fdeb5..b72c2a296b4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java @@ -19,6 +19,7 @@ import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; public class RaceTableWrapper> extends TableWrapper { @@ -27,7 +28,7 @@ extends TableWrapper { public RaceTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter errorReporter, boolean multiSelection) { - super(sailingService, stringMessages, errorReporter, multiSelection, /* enablePager */ false); + super(sailingService, stringMessages, errorReporter, multiSelection, /* enablePager */ false, null /*EntityIdentityComparator for RefreshableSelectionModel*/); Column raceNameColumn = new Column(new AnchorCell()) { @Override @@ -74,4 +75,8 @@ extends TableWrapper { public void setSelectedLeaderboardName(String name) { this.selectedLeaderboardName = name; } + + public RefreshableSelectionModel getRefreshableSelectionModel() { + return (RefreshableSelectionModel) super.getSelectionModel(); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java index 09c7bb16d4b..cb1024fe7eb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java @@ -9,18 +9,20 @@ 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.SelectionModel; -import com.google.gwt.view.client.SingleSelectionModel; 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.SelectionCheckboxColumn; import com.sap.sse.common.Util; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; /** * Implementing classes still have to add the table to the main panel. The table created and wrapped by this object * offers already a {@link ListHandler} for sorting. Subclasses can obtain the table's default column sort handler * created by this class's constructor by calling {@link #getColumnSortHandler}. */ +//TODO change > to > public abstract class TableWrapper> implements IsWidget { protected final CellTable table; private final S selectionModel; @@ -28,6 +30,7 @@ public abstract class TableWrapper> implements Is protected VerticalPanel mainPanel; protected final SailingServiceAsync sailingService; protected final ErrorReporter errorReporter; + private final EntityIdentityComparator entityIdentityComparator; private final AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); private final ListHandler columnSortHandler; @@ -38,7 +41,8 @@ public abstract class TableWrapper> implements Is } public TableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter errorReporter, - boolean multiSelection, boolean enablePager) { + boolean multiSelection, boolean enablePager, EntityIdentityComparator entityIdentityComparator) { + this.entityIdentityComparator = entityIdentityComparator; this.sailingService = sailingService; this.errorReporter = errorReporter; table = new CellTable(10000, tableRes); @@ -49,7 +53,7 @@ public abstract class TableWrapper> implements Is SelectionCheckboxColumn selectionCheckboxColumn = new SelectionCheckboxColumn( tableRes.cellTableStyle().cellTableCheckboxSelected(), tableRes.cellTableStyle().cellTableCheckboxDeselected(), - tableRes.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { + tableRes.cellTableStyle().cellTableCheckboxColumnCell(), this.entityIdentityComparator) { @Override protected ListDataProvider getListDataProvider() { return dataProvider; @@ -63,7 +67,7 @@ public abstract class TableWrapper> implements Is table.addColumn(selectionCheckboxColumn, selectionCheckboxColumn.getHeader()); } else { @SuppressWarnings("unchecked") - S typedSelectionModel = (S) new SingleSelectionModel(); + S typedSelectionModel = (S) new RefreshableSingleSelectionModel(entityIdentityComparator); selectionModel = typedSelectionModel; table.setSelectionModel(selectionModel); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java index 9e6d3b47f9f..f99e62eb864 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java @@ -13,7 +13,7 @@ SingleSelectionModel> { public TrackFileImportDeviceIdentifierTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter errorReporter) { - super(sailingService, stringMessages, errorReporter, /* multiSelection */ false, /* enablePager */ true); + super(sailingService, stringMessages, errorReporter, /* multiSelection */ false, /* enablePager */ true, null /*EntityIdentityComparator for RefreshableSelectionModel*/); TextColumn uuidColumn = new TextColumn() { @Override 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 1296571fbae..4ffd80ab52d 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 @@ -155,7 +155,7 @@ public class TrackedRacesListComposite extends AbstractTrackedRacesListComposite } @Override - protected void makeControlsReactToSelectionChange(List selectedRaces) { + protected void makeControlsReactToSelectionChange(Set selectedRaces) { if(actionButtonsEnabled) { if (selectedRaces.isEmpty()) { btnRemoveRace.setEnabled(false); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java index b987b2512a9..0eb918c74e0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java @@ -13,7 +13,7 @@ import com.sap.sse.gwt.client.ErrorReporter; public class WaypointTableWrapper> extends TableWrapper { public WaypointTableWrapper(boolean multiSelection, SailingServiceAsync sailingService, final StringMessages stringMessages, ErrorReporter errorReporter) { - super(sailingService, stringMessages, errorReporter, multiSelection, true); + super(sailingService, stringMessages, errorReporter, multiSelection, true, null /*EntityIdentityComparator for RefreshableSelectionModel*/); TextColumn nameColumn = new TextColumn() { @Override From 60645aebaf19d23cce3a2933d0fbf7aaaba7e8cd Mon Sep 17 00:00:00 2001 From: fmittag Date: Thu, 26 Nov 2015 17:37:47 +0100 Subject: [PATCH 051/306] Changed the way the busy indicator of the LeaderboardPanel is handled in case it's shown outside of the Panel. A new BusyStateChangeListener is used instead of making the BusyIndicator public. --- .../AbstractLeaderboardFullscreenViewer.java | 8 +++ .../partials/old/EventRegattaLeaderboard.gss | 3 + .../old/EventRegattaLeaderboardResources.java | 1 + .../partials/old/LeaderboardDelegate.java | 1 + .../old/leaderboard/OldLeaderboard.java | 17 +++++- .../old/leaderboard/OldLeaderboard.ui.xml | 4 +- .../multileaderboard/OldMultiLeaderboard.java | 29 +++++---- .../OldMultiLeaderboard.ui.xml | 4 +- .../fullscreen/FullscreenContainer.java | 8 ++- .../fullscreen/FullscreenContainer.ui.xml | 3 + .../leaderboard/ExpandableSortableColumn.java | 8 +-- .../gwt/ui/leaderboard/LeaderboardPanel.java | 59 ++++++++++++------- .../EditableLeaderboardPanel.java | 20 +++---- .../MatchAndApplyScoreCorrectionsDialog.java | 4 +- .../ResultSelectionAndApplyDialog.java | 6 +- .../BusyStateChangeListener.java | 14 +++++ .../busyindicator/BusyStateProvider.java | 13 ++++ .../busyindicator/SimpleBusyIndicator.java | 3 +- .../components/IsEmbeddableComponent.java | 5 -- 19 files changed, 147 insertions(+), 63 deletions(-) create mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateChangeListener.java create mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateProvider.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java index 6e1baf957dd..a9cf371a9d4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java @@ -8,6 +8,8 @@ import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.home.shared.partials.fullscreen.FullscreenContainer; import com.sap.sse.common.Color; +import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; public abstract class AbstractLeaderboardFullscreenViewer extends FullscreenContainer implements LeaderboardDelegate { @@ -19,11 +21,13 @@ public abstract class AbstractLeaderboardFullscreenViewer exte private final Label lastScoringUpdateText = new Label(); protected final Label lastScoringComment = new Label(); protected final Label scoringScheme = new Label(); + protected final BusyIndicator busyIndicator = new SimpleBusyIndicator(); public AbstractLeaderboardFullscreenViewer() { showLogo(); showBorder(); addToolbarInfo(createPanel(lastScoringUpdateText, lastScoringUpdateTime)); + addToolbarBusyIndicator(busyIndicator); addToolbarAction(autoRefreshControl); addToolbarAction(settingsControl); } @@ -88,4 +92,8 @@ public abstract class AbstractLeaderboardFullscreenViewer exte return scoringScheme.getElement(); } + @Override + public Element getBusyIndicatorElement() { + return busyIndicator.getElement(); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss index 7aee44185c8..32ed3aa2a5d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss @@ -106,6 +106,9 @@ background-color: #f0ab00; } +.regattaleaderboard_meta_busyindicator { +} + .regattaleaderboard_meta_liverace { color: red; font-weight: 600; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboardResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboardResources.java index fee597cac95..90bef62da36 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboardResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboardResources.java @@ -30,6 +30,7 @@ public interface EventRegattaLeaderboardResources extends ClientBundle { String regattaleaderboard_meta_update_type(); String regattaleaderboard_meta_scoring_timestamp(); String regattaleaderboard_meta_update_timestamp(); + String regattaleaderboard_meta_busyindicator(); String regattaleaderboard_meta_scoring_text(); String regattaleaderboard_meta_update_text(); String regattaleaderboard_meta_reload(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java index c1523e63036..c52d29d9d6a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java @@ -24,4 +24,5 @@ public interface LeaderboardDelegate { Element getScoringSchemeElement(); + Element getBusyIndicatorElement(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java index d7710ed954b..19bdcedb814 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java @@ -31,12 +31,15 @@ import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; import com.sap.sailing.gwt.ui.leaderboard.ScoringSchemeTypeFormatter; +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.SimpleBusyIndicator; 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.SettingsDialog; -public class OldLeaderboard extends Composite { +public class OldLeaderboard extends Composite implements BusyStateChangeListener { private static OldLeaderboardUiBinder uiBinder = GWT.create(OldLeaderboardUiBinder.class); interface OldLeaderboardUiBinder extends UiBinder { @@ -58,6 +61,7 @@ public class OldLeaderboard extends Composite { private LeaderboardPanel leaderboardPanel; private Timer autoRefreshTimer; private final OldLeaderboardDelegate delegate; + private final BusyIndicator busyIndicator = new SimpleBusyIndicator(); public OldLeaderboard() { this(null); @@ -70,6 +74,7 @@ public class OldLeaderboard extends Composite { settingsAnchor.setTitle(StringMessages.INSTANCE.settings()); autoRefreshAnchor.setTitle(StringMessages.INSTANCE.refresh()); fullscreenAnchor.setTitle(StringMessages.INSTANCE.openFullscreenView()); + busyIndicatorDiv.appendChild(busyIndicator.getElement()); this.delegate = delegate; this.setupFullscreenDelegate(); } @@ -167,7 +172,7 @@ public class OldLeaderboard extends Composite { this.autoRefreshTimer = timer; this.leaderboardPanel = leaderboardPanel; oldLeaderboardPanel.add(leaderboardPanel); - busyIndicatorDiv.appendChild(leaderboardPanel.getBusyIndicator().getElement()); + leaderboardPanel.addBusyStateChangeListener(this); } public void updatedLeaderboard(LeaderboardDTO leaderboard) { @@ -219,8 +224,14 @@ public class OldLeaderboard extends Composite { } lastScoringUpdateTimeDiv.getStyle().setVisibility(Visibility.HIDDEN); } - + + @Override + public void onBusyStateChange(boolean busyState) { + busyIndicator.setBusy(busyState); + } + public interface OldLeaderboardDelegate extends LeaderboardDelegate { Element getHasLiveRaceElement(); } + } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml index 17696bafc13..e6bfdd6861f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml @@ -10,8 +10,8 @@

    -
    -
     
    +
    +
    diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java index f73f6ff4f21..f48660575f8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java @@ -4,7 +4,6 @@ import java.util.Date; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.DivElement; -import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Style.Overflow; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.Style.Visibility; @@ -29,16 +28,19 @@ import com.sap.sailing.gwt.ui.client.DebugIdHelper; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardPanel; -import com.sap.sailing.gwt.ui.leaderboard.SelectedLeaderboardChangeListener; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; import com.sap.sailing.gwt.ui.leaderboard.MultiLeaderboardPanel; import com.sap.sailing.gwt.ui.leaderboard.ScoringSchemeTypeFormatter; +import com.sap.sailing.gwt.ui.leaderboard.SelectedLeaderboardChangeListener; +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.SimpleBusyIndicator; 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.SettingsDialog; -public class OldMultiLeaderboard extends Composite implements SelectedLeaderboardChangeListener { +public class OldMultiLeaderboard extends Composite implements SelectedLeaderboardChangeListener, BusyStateChangeListener { private static OldMultiLeaderboardUiBinder uiBinder = GWT.create(OldMultiLeaderboardUiBinder.class); interface OldMultiLeaderboardUiBinder extends UiBinder { @@ -59,8 +61,8 @@ public class OldMultiLeaderboard extends Composite implements SelectedLeaderboar private MultiLeaderboardPanel multiLeaderboardPanel; private Timer autoRefreshTimer; private final OldMultiLeaderboardDelegate delegate; - - private final DivElement emptyDivElement; + private final BusyIndicator busyIndicator = new SimpleBusyIndicator(); + private LeaderboardPanel lastSelectedLeaderboardPanel; public OldMultiLeaderboard() { this(null); @@ -75,8 +77,8 @@ public class OldMultiLeaderboard extends Composite implements SelectedLeaderboar fullscreenAnchor.setTitle(StringMessages.INSTANCE.openFullscreenView()); this.delegate = delegate; this.setupFullscreenDelegate(); - emptyDivElement = Document.get().createDivElement(); - emptyDivElement.setInnerHTML(" "); + busyIndicatorDiv.appendChild(busyIndicator.getElement()); + lastSelectedLeaderboardPanel = null; } private void setupFullscreenDelegate() { @@ -208,11 +210,18 @@ public class OldMultiLeaderboard extends Composite implements SelectedLeaderboar } } + @Override + public void onBusyStateChange(boolean busyState) { + busyIndicator.setBusy(busyState); + } + @Override public void onSelectedLeaderboardChanged(LeaderboardPanel selectedLeaderboard) { - busyIndicatorDiv.removeAllChildren(); - busyIndicatorDiv.appendChild(selectedLeaderboard.getBusyIndicator().getElement()); - busyIndicatorDiv.appendChild(emptyDivElement); + if(lastSelectedLeaderboardPanel != null) { + lastSelectedLeaderboardPanel.removeBusyStateChangeListener(this); + } + selectedLeaderboard.addBusyStateChangeListener(this); + lastSelectedLeaderboardPanel = selectedLeaderboard; } public interface OldMultiLeaderboardDelegate extends LeaderboardDelegate{ diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml index d29f91043ab..1fa03514f31 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml @@ -9,8 +9,8 @@
    -
    -
     
    +
    +
    diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.java index 998b211f053..8315428bfd4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.java @@ -37,6 +37,7 @@ public class FullscreenContainer { String popup(); String toolbarInfo(); String toolbarAction(); + String toolbarBusyIndicator(); String content(); String contentBorder(); } @@ -88,7 +89,12 @@ public class FullscreenContainer { widget.addStyleName(style.toolbarAction()); toolbarUi.add(widget); } - + + public void addToolbarBusyIndicator(Widget widget) { + widget.addStyleName(style.toolbarBusyIndicator()); + toolbarUi.add(widget); + } + public void addToolbarInfo(Widget widget) { widget.addStyleName(style.toolbarInfo()); toolbarUi.add(widget); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml index bc1f63ed53d..daf2634a4a2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml @@ -50,6 +50,9 @@ cursor: pointer; padding: 10px; } + .toolbarBusyIndicator { + display: inline-block; + } .toolbarAction:hover { background-color: #d79900; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java index 15fad2b3fc8..22a487be9bf 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/ExpandableSortableColumn.java @@ -149,15 +149,15 @@ public abstract class ExpandableSortableColumn extends LeaderboardSortableCol */ public void toggleExpansion() { if (isExpansionEnabled()) { - final boolean oldBusyState = getLeaderboardPanel().getBusyIndicator().isBusy(); - getLeaderboardPanel().getBusyIndicator().setBusy(true); + final boolean oldBusyState = getLeaderboardPanel().isBusy(); + getLeaderboardPanel().setBusyState(true); setTogglingInProcess(true); final CellTable table = getLeaderboardPanel().getLeaderboardTable(); if (isExpanded()) { for (AbstractSortableColumnWithMinMax column : getAllVisibleChildren()) { getLeaderboardPanel().removeColumn(column); // removes only the children currently displayed } - getLeaderboardPanel().getBusyIndicator().setBusy(false); + getLeaderboardPanel().setBusyState(false); setTogglingInProcess(false); // important: toggle expanded state after asking for all visible children setExpanded(!isExpanded()); @@ -177,7 +177,7 @@ public abstract class ExpandableSortableColumn extends LeaderboardSortableCol } getLeaderboardPanel().getLeaderboardTable().redraw(); } - getLeaderboardPanel().getBusyIndicator().setBusy(oldBusyState); + getLeaderboardPanel().setBusyState(oldBusyState); setTogglingInProcess(false); } }); 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 11c7854898e..f3537d8cd7c 100755 --- 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 @@ -102,6 +102,8 @@ 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.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.player.PlayStateListener; import com.sap.sse.gwt.client.player.TimeListener; @@ -122,7 +124,8 @@ import com.sap.sse.gwt.client.useragent.UserAgentDetails; * */ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayStateListener, DisplayedLeaderboardRowsProvider, - Component, IsEmbeddableComponent, CompetitorSelectionChangeListener, LeaderboardFetcher { + Component, IsEmbeddableComponent, CompetitorSelectionChangeListener, LeaderboardFetcher, + BusyStateProvider { public static final String LOAD_LEADERBOARD_DATA_CATEGORY = "loadLeaderboardData"; protected static final NumberFormat scoreFormat = NumberFormat.getFormat("0.##"); @@ -296,6 +299,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS private ImageResource playIcon; private final BusyIndicator busyIndicator; + private final Set busyStateChangeListeners; /** * Tells whether the leaderboard settings were explicitly changed by an external call to @@ -477,8 +481,8 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS break; } - final boolean oldBusyState = getBusyIndicator().isBusy(); - getBusyIndicator().setBusy(true); + final boolean oldBusyState = isBusy(); + setBusyState(true); Runnable doWhenNecessaryDetailHasBeenLoaded = new Runnable() { @Override public void run() { @@ -501,7 +505,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS getLeaderboardTable().sortColumn(raceColumnByRaceName, /* ascending */true); } } - getBusyIndicator().setBusy(oldBusyState); + setBusyState(oldBusyState); } }; if (oldShallAddOverallDetails == shallAddOverallDetails() || oldShallAddOverallDetails || getLeaderboard().hasOverallDetails()) { @@ -1824,7 +1828,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS contentPanel.setStyleName(STYLE_LEADERBOARD_CONTENT); busyIndicator = new SimpleBusyIndicator(false, 0.8f); busyIndicator.ensureDebugId("BusyIndicator"); - + busyStateChangeListeners = new HashSet<>(); // the information panel if (!isEmbedded) { @@ -1920,7 +1924,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS filterClearButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { - getBusyIndicator().setBusy(true); + setBusyState(true); competitorFilterPanel.clearAllActiveFilters(); timeChanged(new Date(), null); } @@ -2237,19 +2241,19 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS @Override public void onSuccess(LeaderboardDTO result) { updateLeaderboard(result); - getBusyIndicator().setBusy(false); + setBusyState(false); } @Override public void onFailure(Throwable caught) { - getBusyIndicator().setBusy(false); + setBusyState(false); getErrorReporter() .reportError("Error trying to obtain leaderboard contents: " + caught.getMessage(), true /* silentMode */); } }); } else { - getBusyIndicator().setBusy(false); + setBusyState(false); } } @@ -2424,7 +2428,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS liveRaceLabel.setVisible(hasLiveRace); } informLeaderboardUpdateListenersAboutLeaderboardUpdated(leaderboard); - getBusyIndicator().setBusy(false); + setBusyState(false); } } @@ -3146,16 +3150,6 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS leaderboardSelectionModel.setSelected(row, false); } } - - @Override - public BusyIndicator getBusyIndicator() { - return busyIndicator; - } - - @Override - public boolean hasBusyIndicator() { - return true; - } private Iterable getSelectedRows() { return leaderboardSelectionModel.getSelectedSet(); @@ -3277,4 +3271,29 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS 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(); + } + + @Override + public void setBusyState(boolean isBusy) { + if (busyIndicator.isBusy() != isBusy) { + busyIndicator.setBusy(isBusy); + for (BusyStateChangeListener listener : busyStateChangeListeners) { + listener.onBusyStateChange(isBusy); + } + } + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/EditableLeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/EditableLeaderboardPanel.java index 78e4426adf6..b6e33545971 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/EditableLeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/EditableLeaderboardPanel.java @@ -319,13 +319,13 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { final RowUpdateWhiteboard whiteboard = new RowUpdateWhiteboard( EditableLeaderboardPanel.this.getData()); getWhiteboardOwner().whiteboardProduced(whiteboard); - getBusyIndicator().setBusy(true); + setBusyState(true); getSailingService().updateLeaderboardMaxPointsReason(getLeaderboardName(), row.competitor.getIdAsString(), raceColumnName, value == null || value.trim().length() == 0 ? null : MaxPointsReason.valueOf(value.trim()), getLeaderboardDisplayDate(), new AsyncCallback>() { @Override public void onFailure(Throwable t) { - getBusyIndicator().setBusy(false); + setBusyState(false); getErrorReporter().reportError( "Error trying to update max points reason for competitor " + row.competitor.getName() + " in leaderboard " + getLeaderboardName() @@ -334,7 +334,7 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { @Override public void onSuccess(Util.Triple newNetAndTotalPointsAndIsCorrected) { - getBusyIndicator().setBusy(false); + setBusyState(false); row.fieldsByRaceColumnName.get(raceColumnName).reasonForMaxPoints = value == null || value.length() == 0 ? null : MaxPointsReason.valueOf(value.trim()); row.fieldsByRaceColumnName.get(raceColumnName).netPoints = newNetAndTotalPointsAndIsCorrected.getA(); @@ -444,14 +444,14 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { final RowUpdateWhiteboard whiteboard = new RowUpdateWhiteboard( EditableLeaderboardPanel.this.getData()); getWhiteboardOwner().whiteboardProduced(whiteboard); - getBusyIndicator().setBusy(true); + setBusyState(true); getSailingService().updateLeaderboardScoreCorrection(getLeaderboardName(), row.competitor.getIdAsString(), raceColumnName, value == null || value.trim().length() == 0 ? null : value.trim().equals("n/a") ? null : Double.valueOf(value.trim()), getLeaderboardDisplayDate(), new AsyncCallback>() { @Override public void onFailure(Throwable t) { - getBusyIndicator().setBusy(false); + setBusyState(false); getErrorReporter().reportError("Error trying to update score correction for competitor "+ row.competitor.getName()+" in leaderboard "+getLeaderboardName()+ " for race "+raceColumnName+": "+t.getMessage()+ @@ -460,7 +460,7 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { @Override public void onSuccess(Util.Triple newNetAndTotalPointsAndIsCorrected) { - getBusyIndicator().setBusy(false); + setBusyState(false); final LeaderboardEntryDTO leaderboardEntryDTO = row.fieldsByRaceColumnName.get(raceColumnName); leaderboardEntryDTO.netPoints = value == null || value.length() == 0 ? newNetAndTotalPointsAndIsCorrected .getA() : Double.valueOf(value.trim()); @@ -523,13 +523,13 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { row.fieldsByRaceColumnName.get(raceColumnName).netPoints, new DialogCallback>() { @Override public void ok(final Util.Pair editedObject) { - getBusyIndicator().setBusy(true); + setBusyState(true); getSailingService().updateLeaderboardScoreCorrection(getLeaderboardName(), row.competitor.getIdAsString(), raceColumnName, editedObject.getB(), getLeaderboardDisplayDate(), new AsyncCallback>() { @Override public void onFailure(Throwable t) { - getBusyIndicator().setBusy(false); + setBusyState(false); getErrorReporter().reportError("Error trying to update score correction for competitor "+ row.competitor.getName()+" in leaderboard "+getLeaderboardName()+ " for race "+raceColumnName+": "+t.getMessage()+ @@ -543,7 +543,7 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { new AsyncCallback>() { @Override public void onFailure(Throwable t) { - getBusyIndicator().setBusy(false); + setBusyState(false); getErrorReporter().reportError("Error trying to update score correction for competitor "+ row.competitor.getName()+" in leaderboard "+getLeaderboardName()+ " for race "+raceColumnName+": "+t.getMessage()+ @@ -552,7 +552,7 @@ public class EditableLeaderboardPanel extends LeaderboardPanel { @Override public void onSuccess(Util.Triple newNetAndTotalPointsAndIsCorrected) { - getBusyIndicator().setBusy(false); + setBusyState(false); final LeaderboardEntryDTO leaderboardEntryDTO = row.fieldsByRaceColumnName.get(raceColumnName); leaderboardEntryDTO.reasonForMaxPoints = editedObject.getA(); leaderboardEntryDTO.netPoints = newNetAndTotalPointsAndIsCorrected.getA(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/MatchAndApplyScoreCorrectionsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/MatchAndApplyScoreCorrectionsDialog.java index 61bf83687bb..066d0212239 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/MatchAndApplyScoreCorrectionsDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/MatchAndApplyScoreCorrectionsDialog.java @@ -409,11 +409,11 @@ public class MatchAndApplyScoreCorrectionsDialog extends DataEntryDialog() { @Override public void onFailure(Throwable caught) { - leaderboardPanel.getBusyIndicator().setBusy(false); + leaderboardPanel.setBusyState(false); errorReporter.reportError(stringMessages.errorUpdatingScoresForLeaderboard(result.getLeaderboardName(), caught.getMessage())); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java index ffe969fe32e..660be8c30ad 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java @@ -214,19 +214,19 @@ public class ResultSelectionAndApplyDialog extends DataEntryDialog() { @Override public void onFailure(Throwable caught) { - leaderboardPanel.getBusyIndicator().setBusy(false); + leaderboardPanel.setBusyState(false); errorReporter.reportError(stringMessages.errorObtainingScoreCorrections(scoreCorrectionProviderName, eventName, boatClassName, timePointWhenResultPublished.toString(), caught.getMessage())); } @Override public void onSuccess(RegattaScoreCorrectionDTO result) { - leaderboardPanel.getBusyIndicator().setBusy(false); + leaderboardPanel.setBusyState(false); new MatchAndApplyScoreCorrectionsDialog(leaderboardPanel, stringMessages, sailingService, errorReporter, result).show(); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateChangeListener.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateChangeListener.java new file mode 100644 index 00000000000..f302065808d --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateChangeListener.java @@ -0,0 +1,14 @@ +package com.sap.sse.gwt.client.controls.busyindicator; + + +/** + * Allows UI components to observe the busy state of an operation. + */ +public interface BusyStateChangeListener { + + /** + * @param busyState the new busy state + */ + void onBusyStateChange(boolean busyState); + +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateProvider.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateProvider.java new file mode 100644 index 00000000000..d886b844853 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyStateProvider.java @@ -0,0 +1,13 @@ +package com.sap.sse.gwt.client.controls.busyindicator; + + +public interface BusyStateProvider { + + void addBusyStateChangeListener(BusyStateChangeListener listener); + + void removeBusyStateChangeListener(BusyStateChangeListener listener); + + void setBusyState(boolean isBusy); + + boolean isBusy(); +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java index 5a607e233d6..f9635ef3e36 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java @@ -1,5 +1,6 @@ package com.sap.sse.gwt.client.controls.busyindicator; +import com.google.gwt.dom.client.Style.Visibility; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.user.client.ui.Image; @@ -39,6 +40,6 @@ public class SimpleBusyIndicator extends BusyIndicator { @Override public void setBusy(boolean busy) { super.setBusy(busy); - busyIndicator.setVisible(busy); + busyIndicator.getElement().getStyle().setVisibility(busy ? Visibility.VISIBLE : Visibility.HIDDEN); } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/IsEmbeddableComponent.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/IsEmbeddableComponent.java index 0f83295b75c..f07d2bf1fbe 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/IsEmbeddableComponent.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/shared/components/IsEmbeddableComponent.java @@ -1,7 +1,6 @@ package com.sap.sse.gwt.client.shared.components; import com.google.gwt.user.client.ui.Widget; -import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; public interface IsEmbeddableComponent { @@ -13,11 +12,7 @@ public interface IsEmbeddableComponent { Widget getLegendWidget(); - BusyIndicator getBusyIndicator(); - boolean isEmbedded(); boolean hasToolbar(); - - boolean hasBusyIndicator(); } From 78916e9e04b9d295d436cb31303aa9e364f79181 Mon Sep 17 00:00:00 2001 From: fmittag Date: Thu, 26 Nov 2015 18:31:50 +0100 Subject: [PATCH 052/306] Added an busy indicator image which works on dark backgorunds (inverted colors) --- .../AbstractLeaderboardFullscreenViewer.java | 6 +++- .../shared/partials/busy/BusyViewImpl.java | 5 +-- .../shared/charts/AbstractRaceChart.java | 4 ++- .../busyindicator/BusyIndicatorResources.java | 3 ++ .../busyindicator/SimpleBusyIndicator.java | 31 ++++++++++++------ .../busy_indicator_circle_inverted.gif | Bin 0 -> 9803 bytes 6 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/busy_indicator_circle_inverted.gif diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java index a9cf371a9d4..aa215bb54d7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java @@ -1,5 +1,6 @@ package com.sap.sailing.gwt.home.desktop.partials.old; +import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.user.client.ui.FlowPanel; @@ -9,6 +10,7 @@ import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.home.shared.partials.fullscreen.FullscreenContainer; import com.sap.sse.common.Color; import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicatorResources; import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; public abstract class AbstractLeaderboardFullscreenViewer extends FullscreenContainer implements @@ -17,11 +19,13 @@ public abstract class AbstractLeaderboardFullscreenViewer exte private final Image autoRefreshControl = new Image("images/home/reload.svg"); private final Image settingsControl = new Image("images/home/settings.svg"); + private final static BusyIndicatorResources RESOURCES = GWT.create(BusyIndicatorResources.class); + private final Label lastScoringUpdateTime = new Label(); private final Label lastScoringUpdateText = new Label(); protected final Label lastScoringComment = new Label(); protected final Label scoringScheme = new Label(); - protected final BusyIndicator busyIndicator = new SimpleBusyIndicator(); + protected final BusyIndicator busyIndicator = new SimpleBusyIndicator(RESOURCES.busyIndicatorCircleInverted()); public AbstractLeaderboardFullscreenViewer() { showLogo(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/busy/BusyViewImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/busy/BusyViewImpl.java index 27c28089e2c..fb3434000bb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/busy/BusyViewImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/busy/BusyViewImpl.java @@ -8,7 +8,8 @@ public class BusyViewImpl extends Composite implements BusyView { public BusyViewImpl() { BusyViewResources.INSTANCE.css().ensureInjected(); - initWidget(new SimpleBusyIndicator(true, 1.0f, "", BusyViewResources.INSTANCE.css().busy())); + SimpleBusyIndicator busyIndicator = new SimpleBusyIndicator(true, 1.0f); + busyIndicator.setImageStyleClass(BusyViewResources.INSTANCE.css().busy()); + initWidget(busyIndicator); } - } 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 64e2ddfae51..49aa6bfbc08 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 @@ -83,7 +83,9 @@ public abstract class AbstractRaceChart extends AbsolutePanel implements RaceSel timeRangeWithZoomProvider.addTimeZoomChangeListener(this); timeRangeWithZoomProvider.addTimeRangeChangeListener(this); chartsCss.ensureInjected(); - busyIndicator = new SimpleBusyIndicator(/* busy */ true, 2.0f, chartsCss.busyIndicatorStyle(), chartsCss.busyIndicatorImageStyle()); + 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()); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicatorResources.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicatorResources.java index 1add987b8c5..b3978b24713 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicatorResources.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/BusyIndicatorResources.java @@ -7,4 +7,7 @@ public interface BusyIndicatorResources extends ClientBundle { @Source("busy_indicator_circle.gif") ImageResource busyIndicatorCircle(); + + @Source("busy_indicator_circle_inverted.gif") + ImageResource busyIndicatorCircleInverted(); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java index f9635ef3e36..1c4254c971d 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/SimpleBusyIndicator.java @@ -13,9 +13,13 @@ public class SimpleBusyIndicator extends BusyIndicator { * The busy indicator component is a circling GIF. */ public SimpleBusyIndicator() { - this(false, 1.0f); + this(false, 1.0f, RESOURCES.busyIndicatorCircle()); } - + + public SimpleBusyIndicator(ImageResource imageResource) { + this(false, 1.0f, imageResource); + } + /** * Creates a new SimpleBusyIndicator with a custom busy state.
    * The busy indicator component is a circling GIF. @@ -24,19 +28,26 @@ public class SimpleBusyIndicator extends BusyIndicator { * @param scale Scales the displayed image. 1.0 is 100%, 0.50 is 50%, ... */ public SimpleBusyIndicator(boolean busy, float scale) { - this(busy, scale, STYLE_NAME_PREFIX + "Simple", STYLE_NAME_PREFIX + "Circle"); + this(busy, scale, RESOURCES.busyIndicatorCircle()); } - - public SimpleBusyIndicator(boolean busy, float scale, String panelCssClass, String imageCssClass) { - this.setStyleName(panelCssClass); - ImageResource resource = RESOURCES.busyIndicatorCircle(); - busyIndicator = new Image(resource.getSafeUri()); - busyIndicator.setStyleName(imageCssClass); - busyIndicator.setPixelSize((int) (resource.getWidth() * scale), (int) (resource.getHeight() * scale)); + + public SimpleBusyIndicator(boolean busy, float scale, ImageResource imageResource) { + this.setStyleName(STYLE_NAME_PREFIX + "Simple"); + busyIndicator = new Image(imageResource.getSafeUri()); + busyIndicator.setStyleName(STYLE_NAME_PREFIX + "Circle"); + busyIndicator.setPixelSize((int) (imageResource.getWidth() * scale), (int) (imageResource.getHeight() * scale)); add(busyIndicator); setBusy(busy); } + public void setPanelStyleClass(String panelCssClass) { + this.setStyleName(panelCssClass); + } + + public void setImageStyleClass(String imageCssClass) { + busyIndicator.setStyleName(imageCssClass); + } + @Override public void setBusy(boolean busy) { super.setBusy(busy); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/busy_indicator_circle_inverted.gif b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/busyindicator/busy_indicator_circle_inverted.gif new file mode 100644 index 0000000000000000000000000000000000000000..d31fd9ec8c095d524586b9c89eebc365a1004b08 GIT binary patch literal 9803 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 T;Z{eSp66H6lLp_Xuq literal 0 HcmV?d00001 From 658701bd756c947d1d0a269aea2f480483f0e1e6 Mon Sep 17 00:00:00 2001 From: fmittag Date: Thu, 26 Nov 2015 18:43:23 +0100 Subject: [PATCH 053/306] set the busy state also for the fullscreen leaderboard --- .../old/AbstractLeaderboardFullscreenViewer.java | 9 +++++++-- .../home/desktop/partials/old/LeaderboardDelegate.java | 2 ++ .../desktop/partials/old/leaderboard/OldLeaderboard.java | 3 +++ .../old/multileaderboard/OldMultiLeaderboard.java | 3 +++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java index aa215bb54d7..16ce11cff0f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/AbstractLeaderboardFullscreenViewer.java @@ -25,13 +25,13 @@ public abstract class AbstractLeaderboardFullscreenViewer exte private final Label lastScoringUpdateText = new Label(); protected final Label lastScoringComment = new Label(); protected final Label scoringScheme = new Label(); - protected final BusyIndicator busyIndicator = new SimpleBusyIndicator(RESOURCES.busyIndicatorCircleInverted()); + protected final BusyIndicator busyIndicator = new SimpleBusyIndicator(false, 0.9f, RESOURCES.busyIndicatorCircleInverted()); public AbstractLeaderboardFullscreenViewer() { showLogo(); showBorder(); - addToolbarInfo(createPanel(lastScoringUpdateText, lastScoringUpdateTime)); addToolbarBusyIndicator(busyIndicator); + addToolbarInfo(createPanel(lastScoringUpdateText, lastScoringUpdateTime)); addToolbarAction(autoRefreshControl); addToolbarAction(settingsControl); } @@ -100,4 +100,9 @@ public abstract class AbstractLeaderboardFullscreenViewer exte public Element getBusyIndicatorElement() { return busyIndicator.getElement(); } + + @Override + public void setBusyState(boolean isBusy) { + busyIndicator.setBusy(isBusy); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java index c52d29d9d6a..fd6ee999728 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/LeaderboardDelegate.java @@ -10,6 +10,8 @@ public interface LeaderboardDelegate { void setLeaderboardPanel(T leaderboardPanel); + void setBusyState(boolean isBusy); + Widget getAutoRefreshControl(); Widget getSettingsControl(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java index 19bdcedb814..3f684efce7e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java @@ -228,6 +228,9 @@ public class OldLeaderboard extends Composite implements BusyStateChangeListener @Override public void onBusyStateChange(boolean busyState) { busyIndicator.setBusy(busyState); + if(delegate != null) { + delegate.setBusyState(busyState); + } } public interface OldLeaderboardDelegate extends LeaderboardDelegate { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java index f48660575f8..c07abafffc3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java @@ -213,6 +213,9 @@ public class OldMultiLeaderboard extends Composite implements SelectedLeaderboar @Override public void onBusyStateChange(boolean busyState) { busyIndicator.setBusy(busyState); + if(delegate != null) { + delegate.setBusyState(busyState); + } } @Override From f20032e86c9e8e6e7302644e9957e77b3bd9eb0b Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Fri, 27 Nov 2015 10:43:41 +0100 Subject: [PATCH 054/306] newSelectionModel worked on LeaderboardConfigPanel --- .../AbstractLeaderboardConfigPanel.java | 54 +++++++++++-------- .../AbstractTrackedRacesListComposite.java | 3 +- .../adminconsole/LeaderboardConfigPanel.java | 11 ++-- .../TrackedRacesListComposite.java | 11 ++-- 4 files changed, 43 insertions(+), 36 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index 4ad8007dbc1..380894dffa6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -24,6 +24,7 @@ import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.VerticalPanel; 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.SetSelectionModel; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaNameAndRaceName; @@ -36,9 +37,6 @@ import com.sap.sailing.gwt.ui.client.LeaderboardsDisplayer; import com.sap.sailing.gwt.ui.client.LeaderboardsRefresher; import com.sap.sailing.gwt.ui.client.ParallelExecutionCallback; import com.sap.sailing.gwt.ui.client.ParallelExecutionHolder; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionModel; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.RegattasDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -58,7 +56,7 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; public abstract class AbstractLeaderboardConfigPanel extends FormPanel implements SelectedLeaderboardProvider, - RegattasDisplayer, RaceSelectionChangeListener, TrackedRaceChangedListener, LeaderboardsDisplayer { + RegattasDisplayer, /*RaceSelectionChangeListener, */TrackedRaceChangedListener, LeaderboardsDisplayer { protected final VerticalPanel mainPanel; protected final TrackedRacesListComposite trackedRacesListComposite; @@ -90,6 +88,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement //protected final RaceSelectionProvider raceSelectionProvider; protected final RefreshableSingleSelectionModel raceSingelSelectionModel; + protected boolean reactOnSelectionChangeEvent; private final LeaderboardsRefresher leaderboardsRefresher; @@ -145,6 +144,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement mainPanel = new VerticalPanel(); mainPanel.setWidth("100%"); this.setWidget(mainPanel); + reactOnSelectionChangeEvent = true; //Create leaderboards list and functionality CaptionPanel leaderboardsCaptionPanel = new CaptionPanel(stringMessages.leaderboards()); @@ -215,12 +215,36 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement trackedRacesCaptionPanel.setStyleName("bold"); //raceSelectionProvider = new RaceSelectionModel(); - trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher, - raceSelectionProvider, stringMessages, /* multiselection */false, isActionButtonsEnabled()); + trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher,/* + raceSelectionProvider, */stringMessages, /* multiselection */false, isActionButtonsEnabled()); trackedRacesListComposite.ensureDebugId("TrackedRacesListComposite"); trackedRacesPanel.add(trackedRacesListComposite); trackedRacesListComposite.addTrackedRaceChangeListener(this); - raceSelectionProvider.addRaceSelectionChangeListener(this); + this.raceSingelSelectionModel = (RefreshableSingleSelectionModel) trackedRacesListComposite.getSelectionModel(); + this.raceSingelSelectionModel.addSelectionChangeHandler(new Handler() { + + @Override + public void onSelectionChange(SelectionChangeEvent event) { + if (reactOnSelectionChangeEvent) { + // if no leaderboard column is selected, ignore the race selection change + RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceColumnAndFleetName = getSelectedRaceColumnWithFleet(); + if (selectedRaceColumnAndFleetName != null) { + if (raceSingelSelectionModel.getSelectedSet().isEmpty()) { + if (selectedRaceColumnAndFleetName.getA() + .getRaceIdentifier(selectedRaceColumnAndFleetName.getB()) != null) { + unlinkRaceColumnFromTrackedRace( + selectedRaceColumnAndFleetName.getA().getRaceColumnName(), + selectedRaceColumnAndFleetName.getB()); + } + } else { + linkTrackedRaceToSelectedRaceColumn(selectedRaceColumnAndFleetName.getA(), + selectedRaceColumnAndFleetName.getB(), + raceSingelSelectionModel.getSelectedObject().getRaceIdentifier()); + } + } + } + } + }); Button reloadAllRaceLogs = new Button(stringMessages.reloadAllRaceLogs()); reloadAllRaceLogs.ensureDebugId("ReloadAllRaceLogsButton"); @@ -484,22 +508,6 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement raceColumnAndFleetName.getA().setRaceIdentifier(raceColumnAndFleetName.getB(), null); } - @Override - public void onRaceSelectionChange(List selectedRaces) { - // if no leaderboard column is selected, ignore the race selection change - RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceColumnAndFleetName = getSelectedRaceColumnWithFleet(); - if (selectedRaceColumnAndFleetName != null) { - if (selectedRaces.isEmpty()) { - if (selectedRaceColumnAndFleetName.getA().getRaceIdentifier(selectedRaceColumnAndFleetName.getB()) != null) { - unlinkRaceColumnFromTrackedRace(selectedRaceColumnAndFleetName.getA().getRaceColumnName(), selectedRaceColumnAndFleetName.getB()); - } - } else { - linkTrackedRaceToSelectedRaceColumn(selectedRaceColumnAndFleetName.getA(), selectedRaceColumnAndFleetName.getB(), - selectedRaces.iterator().next()); - } - } - } - private void linkTrackedRaceToSelectedRaceColumn(final RaceColumnDTO selectedRaceInLeaderboard, final FleetDTO fleet, final RegattaAndRaceIdentifier selectedRace) { sailingService.connectTrackedRaceToLeaderboardColumn(getSelectedLeaderboardName(), selectedRaceInLeaderboard 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 1acaf12b83c..fba15b12e13 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 @@ -31,7 +31,6 @@ 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.RaceSelectionProvider; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.RegattasDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -92,7 +91,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl public AbstractTrackedRacesListComposite(final SailingServiceAsync sailingService, final ErrorReporter errorReporter, final RegattaRefresher regattaRefresher, - final RaceSelectionProvider raceSelectionProvider, final StringMessages stringMessages, boolean hasMultiSelection) { + /*final RaceSelectionProvider raceSelectionProvider, */final StringMessages stringMessages, boolean hasMultiSelection) { this.sailingService = sailingService; this.errorReporter = errorReporter; this.regattaRefresher = regattaRefresher; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java index 0fa5839fa4e..02f30f97ae6 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java @@ -13,8 +13,6 @@ import java.util.Set; import com.google.gwt.cell.client.AbstractCell; import com.google.gwt.cell.client.FieldUpdater; 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.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.safehtml.client.SafeHtmlTemplates; @@ -40,7 +38,6 @@ import com.sap.sailing.gwt.ui.adminconsole.DisablableCheckboxCell.IsEnabled; import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; import com.sap.sailing.gwt.ui.client.LeaderboardsDisplayer; import com.sap.sailing.gwt.ui.client.LeaderboardsRefresher; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.RegattasDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -58,7 +55,7 @@ import com.sap.sse.gwt.client.URLEncoder; import com.sap.sse.gwt.client.async.MarkedAsyncCallback; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; -public class LeaderboardConfigPanel extends AbstractLeaderboardConfigPanel implements SelectedLeaderboardProvider, RegattasDisplayer, RaceSelectionChangeListener, +public class LeaderboardConfigPanel extends AbstractLeaderboardConfigPanel implements SelectedLeaderboardProvider, RegattasDisplayer, /*RaceSelectionChangeListener,*/ TrackedRaceChangedListener, LeaderboardsDisplayer { private final AnchorTemplates ANCHORTEMPLATE = GWT.create(AnchorTemplates.class); @@ -673,16 +670,19 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { @Override protected void leaderboardSelectionChanged() { // make sure that clearing the selection doesn't cause an unlinking of the selected tracked race - raceSelectionProvider.removeRaceSelectionChangeListener(this); + reactOnSelectionChangeEvent = false; + //raceSelectionProvider.removeRaceSelectionChangeListener(this); trackedRacesListComposite.clearSelection(); // add listener again using a scheduled command which is executed when the browser's event loop re-gains // control; we assume that at that point in time the selection updates have already been performed + /* Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { raceSelectionProvider.addRaceSelectionChangeListener(LeaderboardConfigPanel.this); } }); + */ leaderboardRemoveButton.setEnabled(!refreshableLeaderboardSelectionModel.getSelectedSet().isEmpty()); StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard(); if (refreshableLeaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { @@ -705,6 +705,7 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { trackedRacesCaptionPanel.setVisible(false); selectedRaceInLeaderboard = null; } + reactOnSelectionChangeEvent = true; } private void createFlexibleLeaderboard() { 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 4ffd80ab52d..005d6ecb3b3 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 @@ -13,7 +13,6 @@ 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.RaceSelectionProvider; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.RegattasDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -38,9 +37,9 @@ public class TrackedRacesListComposite extends AbstractTrackedRacesListComposite private boolean actionButtonsEnabled; public TrackedRacesListComposite(final SailingServiceAsync sailingService, final ErrorReporter errorReporter, - final RegattaRefresher regattaRefresher, RaceSelectionProvider raceSelectionProvider, + final RegattaRefresher regattaRefresher, /*RaceSelectionProvider raceSelectionProvider,*/ final StringMessages stringMessages, boolean hasMultiSelection, boolean actionButtonsEnabled) { - super(sailingService, errorReporter, regattaRefresher, raceSelectionProvider, stringMessages, hasMultiSelection); + super(sailingService, errorReporter, regattaRefresher/*, raceSelectionProvider*/, stringMessages, hasMultiSelection); this.raceIsTrackedRaceChangeListener = new HashSet(); this.actionButtonsEnabled = actionButtonsEnabled; createUI(); @@ -113,7 +112,7 @@ public class TrackedRacesListComposite extends AbstractTrackedRacesListComposite btnRemoveRace.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { - removeAndUntrackRaces(getSelectedRaces()); + removeAndUntrackRaces(refreshableSelectionModel.getSelectedSet()); } }); btnRemoveRace.setEnabled(false); @@ -124,7 +123,7 @@ public class TrackedRacesListComposite extends AbstractTrackedRacesListComposite btnUntrack.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent click) { - stopTrackingRaces(getSelectedRaces()); + stopTrackingRaces(refreshableSelectionModel.getSelectedSet()); } }); btnUntrack.setEnabled(false); @@ -146,7 +145,7 @@ public class TrackedRacesListComposite extends AbstractTrackedRacesListComposite btnExport.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { - exportPopup.center(getSelectedRaces()); + exportPopup.center(new ArrayList<>(refreshableSelectionModel.getSelectedSet())); } }); btnExport.setEnabled(false); From d5710e1245edf2d765f32a1601a2600bcbfd8d9c Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Fri, 27 Nov 2015 12:46:07 +0100 Subject: [PATCH 055/306] deleted RaceSelectionModel in the WindPanel --- .../gwt/ui/adminconsole/WindPanel.java | 52 ++++++++++--------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java index adda238a59d..3e5ef145f66 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java @@ -45,15 +45,13 @@ import com.google.gwt.user.client.ui.TabPanel; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.view.client.ListDataProvider; +import com.google.gwt.view.client.SelectionChangeEvent; 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.RaceDTO; import com.sap.sailing.domain.common.impl.WindSourceImpl; import com.sap.sailing.gwt.ui.adminconsole.WindImportResult.RaceEntry; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionModel; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.RegattasDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -66,6 +64,7 @@ import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; /** @@ -76,7 +75,7 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; * @author Axel Uhl (d043530) * */ -public class WindPanel extends FormPanel implements RegattasDisplayer, WindShower, RaceSelectionChangeListener { +public class WindPanel extends FormPanel implements RegattasDisplayer, WindShower/*, RaceSelectionChangeListener*/ { private static final String EXPEDITON_IMPORT_PARAMETER_RACES = "races"; @@ -93,7 +92,8 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe private final TextColumn windDirectionInDegColumn; private final TextColumn positionColumn; private final TrackedRacesListComposite trackedRacesListComposite; - private final RaceSelectionProvider raceSelectionProvider; +// private final RaceSelectionProvider raceSelectionProvider; + private final RefreshableMultiSelectionModel refreshableRaceSelectionModel; private final WindSourcesToExcludeSelectorPanel windSourcesToExcludeSelectorPanel; private final CheckBox raceIsKnownToStartUpwindBox; private final CaptionPanel windCaptionPanel; @@ -108,17 +108,26 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe this.sailingService = sailingService; this.errorReporter = errorReporter; this.stringMessages = stringMessages; - this.raceSelectionProvider = new RaceSelectionModel(); +// this.raceSelectionProvider = new RaceSelectionModel(); windSourcesToExcludeSelectorPanel = new WindSourcesToExcludeSelectorPanel(sailingService, stringMessages, errorReporter); VerticalPanel mainPanel = new VerticalPanel(); mainPanel.setSize("100%", "100%"); this.setWidget(mainPanel); - trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher, - raceSelectionProvider, stringMessages, /*multiselection*/true, /* actionButtonsEnabled */ false); + trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher/*, + raceSelectionProvider*/, stringMessages, /*multiselection*/true, /* actionButtonsEnabled */ false); mainPanel.add(trackedRacesListComposite); - raceSelectionProvider.addRaceSelectionChangeListener(this); + //raceSelectionProvider.addRaceSelectionChangeListener(this); + refreshableRaceSelectionModel = (RefreshableMultiSelectionModel) trackedRacesListComposite.getSelectionModel(); + refreshableRaceSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + + @Override + public void onSelectionChange(SelectionChangeEvent event) { + // TODO Auto-generated method stub + updateWindDisplay(); + } + }); windCaptionPanel = new CaptionPanel(stringMessages.wind()); windCaptionPanel.setVisible(false); @@ -180,8 +189,8 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe removeColumn = new IdentityColumn(new ActionCell(stringMessages.remove(), new Delegate() { @Override public void execute(final WindDTO wind) { - List selectedRaces = raceSelectionProvider.getSelectedRaces(); - final RegattaAndRaceIdentifier raceIdentifier = selectedRaces.get(selectedRaces.size() - 1); + List selectedRaces = new ArrayList<>(refreshableRaceSelectionModel.getSelectedSet()); + final RegattaAndRaceIdentifier raceIdentifier = selectedRaces.get(selectedRaces.size() - 1).getRaceIdentifier(); sailingService.removeWind(raceIdentifier, wind, new AsyncCallback() { @Override public void onSuccess(Void result) { @@ -261,14 +270,14 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe @Override public void onClick(ClickEvent event) { final String warningMessage; - if (trackedRacesListComposite.getSelectedRaces().isEmpty()) { + if (refreshableRaceSelectionModel.getSelectedSet().isEmpty()) { warningMessage = stringMessages.windImport_AllRacesWarning(); } else { - warningMessage = stringMessages.windImport_SelectedRacesWarning(trackedRacesListComposite.getSelectedRaces().size()); + warningMessage = stringMessages.windImport_SelectedRacesWarning(refreshableRaceSelectionModel.getSelectedSet().size()); } if (Window.confirm(warningMessage)) { resultReport.setText(stringMessages.loading()); - sailingService.importWindFromIgtimi(trackedRacesListComposite.getSelectedRaces(), + sailingService.importWindFromIgtimi(new ArrayList<>(refreshableRaceSelectionModel.getSelectedSet()), correctByDeclination.getValue(), new AsyncCallback>() { @Override public void onFailure(Throwable caught) { @@ -375,13 +384,13 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe form.addSubmitHandler(new FormPanel.SubmitHandler() { public void onSubmit(SubmitEvent event) { if ((fileUpload.getFilename() != null) && (fileUpload.getFilename().trim().length() > 0)) { - List selectedRaces = raceSelectionProvider.getSelectedRaces(); + Set selectedRaces = refreshableRaceSelectionModel.getSelectedSet(); String warningMessage; if (selectedRaces.size() > 0) { warningMessage = stringMessages.windImport_SelectedRacesWarning(selectedRaces.size()); RaceSelection raceSelection = RaceSelection.create(); - for (int i = 0; i < selectedRaces.size(); i++) { - RegattaAndRaceIdentifier raceIdentifier = selectedRaces.get(i); + for (RaceDTO race : selectedRaces) { + RegattaAndRaceIdentifier raceIdentifier = race.getRaceIdentifier(); RaceSelection.RaceEntry raceEntry = RaceSelection.RaceEntry.create(); raceEntry.setRaceName(raceIdentifier.getRaceName()); raceEntry.setRegatteName(raceIdentifier.getRegattaName()); @@ -557,12 +566,12 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe } private RegattaAndRaceIdentifier getSelectedRace() { - List selectedRaces = raceSelectionProvider.getSelectedRaces(); + Set selectedRaces = refreshableRaceSelectionModel.getSelectedSet(); if (selectedRaces.isEmpty() || selectedRaces.size() > 1) { return null; } - return selectedRaces.get(0); + return selectedRaces.iterator().next().getRaceIdentifier(); } private void clearWindSources() { @@ -626,9 +635,4 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe } } - - @Override - public void onRaceSelectionChange(List selectedRaces) { - updateWindDisplay(); - } } From 72eb78a18ed2ca198feb64048277bd9ac7102efc Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Fri, 27 Nov 2015 13:02:56 +0100 Subject: [PATCH 056/306] deleted RaceSelctionModel from AssignToMediaDialog --- .../ui/adminconsole/AssignRacesToMediaDialog.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AssignRacesToMediaDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AssignRacesToMediaDialog.java index 2726be874cf..183b6b7f50d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AssignRacesToMediaDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AssignRacesToMediaDialog.java @@ -1,7 +1,6 @@ package com.sap.sailing.gwt.ui.adminconsole; import java.util.HashSet; -import java.util.List; import java.util.Set; import com.google.gwt.user.client.ui.Button; @@ -14,7 +13,6 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.dto.RaceDTO; import com.sap.sailing.domain.common.dto.TrackedRaceDTO; import com.sap.sailing.domain.common.media.MediaTrack; -import com.sap.sailing.gwt.ui.client.RaceSelectionModel; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.RegattasDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -39,8 +37,8 @@ public class AssignRacesToMediaDialog extends DataEntryDialog selectedRaces) { } - +*/ @Override protected void makeControlsReactToFillRegattas(Iterable regattas) { } @@ -114,7 +112,7 @@ public class AssignRacesToMediaDialog extends DataEntryDialog getAssignedRaces() { - List races = trackedRacesListComposite.getSelectedRaces(); + Set races = trackedRacesListComposite.getSelectionModel().getSelectedSet(); Set assignedRaces = new HashSet(); for (RaceDTO race : races) { assignedRaces.add(race.getRaceIdentifier()); From 171d77d6c99a1083cb127fbf6bee64c0780f6739 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Fri, 27 Nov 2015 13:33:49 +0100 Subject: [PATCH 057/306] changed constructor of AbstractEventManagemantPanel --- .../AbstractEventManagementPanel.java | 7 +++--- .../AbstractRaceManagementPanel.java | 23 ++++++++++++------- .../SwissTimingEventManagementPanel.java | 3 +-- .../SwissTimingReplayConnectorPanel.java | 3 +-- .../TracTracEventManagementPanel.java | 3 +-- .../gwt/ui/adminconsole/WindPanel.java | 1 - 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java index 326230751e8..2f350499fa8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java @@ -15,7 +15,6 @@ import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.ListBox; import com.sap.sailing.domain.common.BoatClassMasterdata; import com.sap.sailing.gwt.ui.client.AbstractRegattaPanel; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -30,7 +29,7 @@ public abstract class AbstractEventManagementPanel extends AbstractRegattaPanel private final ListBox availableRegattasListBox; public AbstractEventManagementPanel(SailingServiceAsync sailingService, RegattaRefresher regattaRefresher, - ErrorReporter errorReporter, RaceSelectionProvider raceSelectionProvider, boolean actionButtonsEnabled, StringMessages stringMessages) { + ErrorReporter errorReporter/*, RaceSelectionProvider raceSelectionProvider*/, boolean actionButtonsEnabled, StringMessages stringMessages) { super(sailingService, regattaRefresher, errorReporter, stringMessages); this.availableRegattas = new ArrayList(); @@ -38,8 +37,8 @@ public abstract class AbstractEventManagementPanel extends AbstractRegattaPanel this.availableRegattasListBox.ensureDebugId("AvailableRegattasListBox"); // TrackedEventsComposite should exist in every *ManagementPanel. - trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher, - raceSelectionProvider, stringMessages, /* multiselection */ true, actionButtonsEnabled); + trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher/*, + raceSelectionProvider*/, stringMessages, /* multiselection */ true, actionButtonsEnabled); trackedRacesListComposite.ensureDebugId("TrackedRacesListComposite"); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java index a6104f78418..c240585f823 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java @@ -1,20 +1,21 @@ package com.sap.sailing.gwt.ui.adminconsole; import java.util.List; +import java.util.Set; import com.google.gwt.user.client.ui.CaptionPanel; import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.dto.RaceDTO; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionModel; import com.sap.sailing.gwt.ui.client.RegattaRefresher; 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.gwt.client.ErrorReporter; -public abstract class AbstractRaceManagementPanel extends AbstractEventManagementPanel implements RaceSelectionChangeListener { +public abstract class AbstractRaceManagementPanel extends AbstractEventManagementPanel /*implements RaceSelectionChangeListener*/ { protected RegattaAndRaceIdentifier singleSelectedRace; protected RaceDTO selectedRaceDTO; @@ -27,7 +28,7 @@ public abstract class AbstractRaceManagementPanel extends AbstractEventManagemen public AbstractRaceManagementPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, RegattaRefresher regattaRefresher, boolean actionButtonsEnabled, StringMessages stringMessages) { - super(sailingService, regattaRefresher, errorReporter, new RaceSelectionModel(), actionButtonsEnabled, stringMessages); + super(sailingService, regattaRefresher, errorReporter, /*new RaceSelectionModel(), */actionButtonsEnabled, stringMessages); VerticalPanel mainPanel = new VerticalPanel(); this.setWidget(mainPanel); @@ -35,7 +36,14 @@ public abstract class AbstractRaceManagementPanel extends AbstractEventManagemen mainPanel.add(trackedRacesListComposite); - trackedRacesListComposite.addRaceSelectionChangeListener(this); + //trackedRacesListComposite.addRaceSelectionChangeListener(this); + trackedRacesListComposite.getSelectionModel().addSelectionChangeHandler(new Handler() { + + @Override + public void onSelectionChange(SelectionChangeEvent event) { + onRaceSelectionChange(trackedRacesListComposite.getSelectionModel().getSelectedSet()); + } + }); singleSelectedRace = null; @@ -51,10 +59,9 @@ public abstract class AbstractRaceManagementPanel extends AbstractEventManagemen abstract void refreshSelectedRaceData(); - @Override - public void onRaceSelectionChange(List selectedRaces) { + public void onRaceSelectionChange(Set selectedRaces) { if (selectedRaces.size() == 1) { - singleSelectedRace = selectedRaces.get(0); + singleSelectedRace = selectedRaces.iterator().next().getRaceIdentifier(); selectedCaptionRacePanel.setCaptionText(singleSelectedRace.getRaceName()); selectedCaptionRacePanel.setVisible(true); for (RegattaDTO regatta : getAvailableRegattas()) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java index 06507532f71..b4c00af416e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java @@ -33,7 +33,6 @@ import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.MultiSelectionModel; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.RegattaName; -import com.sap.sailing.gwt.ui.client.RaceSelectionModel; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -66,7 +65,7 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane public SwissTimingEventManagementPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, RegattaRefresher regattaRefresher, StringMessages stringConstants) { - super(sailingService, regattaRefresher, errorReporter, new RaceSelectionModel(), true, stringConstants); + super(sailingService, regattaRefresher, errorReporter, /*new RaceSelectionModel(),*/ true, stringConstants); this.errorReporter = errorReporter; VerticalPanel mainPanel = new VerticalPanel(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java index 341e4c17ac1..69e335d4ab3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java @@ -33,7 +33,6 @@ import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.MultiSelectionModel; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.RegattaName; -import com.sap.sailing.gwt.ui.client.RaceSelectionModel; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -64,7 +63,7 @@ public class SwissTimingReplayConnectorPanel extends AbstractEventManagementPane public SwissTimingReplayConnectorPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, RegattaRefresher regattaRefresher, StringMessages stringMessages) { - super(sailingService, regattaRefresher, errorReporter, new RaceSelectionModel(), true, stringMessages); + super(sailingService, regattaRefresher, errorReporter, /*new RaceSelectionModel(),*/ true, stringMessages); this.errorReporter = errorReporter; availableSwissTimingRaces = new ArrayList(); previousConfigurationsComboBox = new ListBox(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java index 4f31ef5305e..4c1ce8e59e7 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java @@ -36,7 +36,6 @@ import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.SelectionModel; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.RegattaName; -import com.sap.sailing.gwt.ui.client.RaceSelectionModel; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -86,7 +85,7 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { public TracTracEventManagementPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, RegattaRefresher regattaRefresher, StringMessages stringMessages) { - super(sailingService, regattaRefresher, errorReporter, new RaceSelectionModel(), true, stringMessages); + super(sailingService, regattaRefresher, errorReporter, /*new RaceSelectionModel(),*/ true, stringMessages); this.errorReporter = errorReporter; this.previousConfigurations = new HashMap(); this.availableTracTracRaces = new ArrayList(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java index 3e5ef145f66..6d9872bda52 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java @@ -124,7 +124,6 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe @Override public void onSelectionChange(SelectionChangeEvent event) { - // TODO Auto-generated method stub updateWindDisplay(); } }); From f0f796e2505ee2438bafbc65ebce2f390163b1ce Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Fri, 27 Nov 2015 14:13:45 +0100 Subject: [PATCH 058/306] resolved NullPointerExeption in AbstracetTrackedRacesListComposite.java --- .../AbstractTrackedRacesListComposite.java | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) 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 fba15b12e13..a2b7a6b628f 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 @@ -102,6 +102,29 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl 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(); + setWidget(panel); + + HorizontalPanel filterPanel = new HorizontalPanel(); + panel.add(filterPanel); + Label lblFilterRaces = new Label(stringMessages.filterRacesByName() + ":"); + lblFilterRaces.setWordWrap(false); + filterPanel.setSpacing(5); + filterPanel.add(lblFilterRaces); + filterPanel.setCellVerticalAlignment(lblFilterRaces, HasVerticalAlignment.ALIGN_MIDDLE); + noTrackedRacesLabel = new Label(stringMessages.noRacesYet()); + noTrackedRacesLabel.setWordWrap(false); + panel.add(noTrackedRacesLabel); + + AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); + raceTable = new CellTable(/* pageSize */10000, tableRes); + raceTable.ensureDebugId("TrackedRacesCellTable"); + if (multiSelection) { this.selectionCheckboxColumn = new SelectionCheckboxColumn( tableResources.cellTableStyle().cellTableCheckboxSelected(), @@ -130,28 +153,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl refreshableSelectionModel = new RefreshableSingleSelectionModel(); raceTable.setSelectionModel(refreshableSelectionModel); } - - raceList = new ListDataProvider(); - settings = new TrackedRacesSettings(); - settings.setDelayToLiveInSeconds(DEFAULT_LIVE_DELAY_IN_MILLISECONDS / 1000l); - - VerticalPanel panel = new VerticalPanel(); - setWidget(panel); - - HorizontalPanel filterPanel = new HorizontalPanel(); - panel.add(filterPanel); - Label lblFilterRaces = new Label(stringMessages.filterRacesByName() + ":"); - lblFilterRaces.setWordWrap(false); - filterPanel.setSpacing(5); - filterPanel.add(lblFilterRaces); - filterPanel.setCellVerticalAlignment(lblFilterRaces, HasVerticalAlignment.ALIGN_MIDDLE); - noTrackedRacesLabel = new Label(stringMessages.noRacesYet()); - noTrackedRacesLabel.setWordWrap(false); - panel.add(noTrackedRacesLabel); - - AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); - raceTable = new CellTable(/* pageSize */10000, tableRes); - raceTable.ensureDebugId("TrackedRacesCellTable"); + ListHandler columnSortHandler = setupTableColumns(stringMessages); raceTable.setWidth("300px"); From b3c6438bc53721854b24406842104b51a34d3379 Mon Sep 17 00:00:00 2001 From: "U-GLOBAL\\D060458" Date: Fri, 27 Nov 2015 18:38:10 +0100 Subject: [PATCH 059/306] Added an advantageLine animator that moves the line in 100ms steps --- .../gwt/ui/client/shared/racemap/RaceMap.java | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) 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 b4657f0d566..5124e2a0d3a 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 @@ -50,6 +50,7 @@ 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.mvc.MVCArrayCallback; import com.google.gwt.maps.client.overlays.InfoWindow; import com.google.gwt.maps.client.overlays.InfoWindowOptions; import com.google.gwt.maps.client.overlays.Marker; @@ -81,6 +82,8 @@ 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.impl.NauticalMileDistance; 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.GetPolarAction; @@ -915,7 +918,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); // 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); + //showAdvantageLine(competitorsToShow, newTime); // Rezoom the map LatLngBounds zoomToBounds = null; @@ -1221,6 +1224,51 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe } return null; } + + private class AdvantageLineAnimator extends com.google.gwt.user.client.Timer { + private static final double MILLIS_TO_HOURS_FACTOR = 3600000; + + private DelegateCoordinateSystem coordinateSystem; + private GPSFixDTO lastBoatFix; + private long lastTime; + + public AdvantageLineAnimator(DelegateCoordinateSystem coordinateSystem) { + this.coordinateSystem = coordinateSystem; + this.scheduleRepeating(100); + this.lastTime = System.currentTimeMillis(); + } + + public void setLastFix(GPSFixDTO lastBoatFix) { + this.lastBoatFix = lastBoatFix; + } + + @Override + public void run() { + long time = System.currentTimeMillis(); + long deltaTime = time - lastTime; + lastTime = time; + + if (lastBoatFix != null) { + Position oldPosition = lastBoatFix.position; + Position newPosition = oldPosition.translateRhumb( + new DegreeBearingImpl(lastBoatFix.speedWithBearing.bearingInDegrees), + new NauticalMileDistance(deltaTime / MILLIS_TO_HOURS_FACTOR * lastBoatFix.speedWithBearing.speedInKnots)); + + final double latDelta = newPosition.getLatDeg() - oldPosition.getLatDeg(); + final double lngDelta = newPosition.getLngDeg() - oldPosition.getLngDeg(); + + final MVCArray path = advantageLine.getPath(); + advantageLine.getPath().forEach(new MVCArrayCallback() { + @Override + public void forEach(LatLng element, int index) { + path.setAt(index, LatLng.newInstance(element.getLatitude() + latDelta, element.getLongitude() + lngDelta)); + } + }); + } + } + } + + private AdvantageLineAnimator advantageTimer = new AdvantageLineAnimator(coordinateSystem); private void showAdvantageLine(Iterable competitorsToShow, Date date) { if (map != null && lastRaceTimesInfo != null && quickRanks != null && lastCombinedWindTrackInfoDTO != null) { @@ -1317,14 +1365,13 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe }); advantageLine.setMap(map); } else { - advantageLine.getPath().removeAt(1); - advantageLine.getPath().removeAt(0); - advantageLine.getPath().insertAt(0, advantageLinePos1); - advantageLine.getPath().insertAt(1, advantageLinePos2); + advantageLine.getPath().setAt(0, advantageLinePos1); + advantageLine.getPath().setAt(1, advantageLinePos2); advantageLineMouseOverHandler.setTrueWindBearing(bearingOfCombinedWindInDeg); advantageLineMouseOverHandler.setDate(new Date(windFix.measureTimepoint)); } drawAdvantageLine = true; + advantageTimer.setLastFix(lastBoatFix); } } if (!drawAdvantageLine) { From b4ef4605d42131905ddf54e9f1ab57e539b120ae Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 30 Nov 2015 10:24:11 +0100 Subject: [PATCH 060/306] inserted the RefreshableMultiSelectionModel into LeaderboadGroupConfigPanel and revised the RefreshableSelectionModels --- .../LeaderboardGroupConfigPanel.java | 93 +++++++++---------- .../RefreshableMultiSelectionModel.java | 6 +- .../RefreshableSingleSelectionModel.java | 6 +- 3 files changed, 52 insertions(+), 53 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java index fe6b8c92c7d..13b99345245 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java @@ -7,8 +7,6 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.UUID; - import com.google.gwt.cell.client.FieldUpdater; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; @@ -30,7 +28,6 @@ import com.google.gwt.user.client.ui.TextArea; 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.MultiSelectionModel; import com.google.gwt.view.client.ProvidesKey; import com.google.gwt.view.client.SelectionChangeEvent; import com.sap.sailing.domain.common.dto.RaceColumnDTO; @@ -55,6 +52,8 @@ import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.URLEncoder; import com.sap.sse.gwt.client.async.MarkedAsyncCallback; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -76,7 +75,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements private LabeledAbstractFilterablePanel groupsFilterablePanel; private Button removeButton; private CellTable groupsTable; - private MultiSelectionModel groupsSelectionModel; + private RefreshableMultiSelectionModel refreshableGroupsSelectionModel; /** * True, if only one group is selected in the {@link #groupsSelectionModel}. */ @@ -84,7 +83,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements private ListDataProvider groupsProvider; private CellTable groupDetailsTable; - private MultiSelectionModel groupDetailsSelectionModel; + private RefreshableMultiSelectionModel refreshableGroupDetailsSelectionModel; private ListDataProvider groupDetailsProvider; private Button editDescriptionButton; private Button abortDescriptionButton; @@ -95,7 +94,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements private LabeledAbstractFilterablePanel leaderboardsFilterablePanel; private CellTable leaderboardsTable; - private MultiSelectionModel leaderboardsSelectionModel; + private RefreshableMultiSelectionModel refreshableLeaderboardsSelectionModel; private ListDataProvider leaderboardsProvider; private Button moveToLeaderboardsButton; @@ -198,8 +197,8 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements return leaderboardsProvider; } }; - leaderboardsSelectionModel = leaderboardTableSelectionColumn.getSelectionModel(); - leaderboardsTable.setSelectionModel(leaderboardsSelectionModel, leaderboardTableSelectionColumn.getSelectionManager()); + refreshableLeaderboardsSelectionModel = leaderboardTableSelectionColumn.getSelectionModel(); + leaderboardsTable.setSelectionModel(refreshableLeaderboardsSelectionModel, leaderboardTableSelectionColumn.getSelectionManager()); leaderboardsFilterablePanel = new LabeledAbstractFilterablePanel(filterLeaderboardsLabel, availableLeaderboards, leaderboardsTable, leaderboardsProvider) { @Override public Iterable getSearchableStrings(StrippedLeaderboardDTO t) { @@ -258,10 +257,10 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements leaderboardsTable.addColumn(leaderboardsNameColumn, stringMessages.leaderboardName()); leaderboardsTable.addColumn(leaderboardsRacesColumn, stringMessages.races()); - leaderboardsSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + refreshableLeaderboardsSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { - Set selectedLeaderboards = leaderboardsSelectionModel.getSelectedSet(); + Set selectedLeaderboards = refreshableLeaderboardsSelectionModel.getSelectedSet(); moveToGroupButton.setEnabled(selectedLeaderboards != null && selectedLeaderboards.size() > 0); } }); @@ -377,17 +376,17 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements groupDetailsTable.addColumn(groupDetailsNameColumn, stringMessages.leaderboardName()); groupDetailsTable.addColumn(groupDetailsRacesColumn, stringMessages.races()); - groupDetailsSelectionModel = groupDetailsTableSelectionColumn.getSelectionModel(); - groupDetailsSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + refreshableGroupDetailsSelectionModel = groupDetailsTableSelectionColumn.getSelectionModel(); + refreshableGroupDetailsSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { - Set selectedLeaderboardsInGroup = groupDetailsSelectionModel.getSelectedSet(); + Set selectedLeaderboardsInGroup = refreshableGroupDetailsSelectionModel.getSelectedSet(); moveToLeaderboardsButton.setEnabled(selectedLeaderboardsInGroup != null && !selectedLeaderboardsInGroup.isEmpty()); leaderboardDownButton.setEnabled(selectedLeaderboardsInGroup != null && selectedLeaderboardsInGroup.size() == 1); leaderboardUpButton.setEnabled(selectedLeaderboardsInGroup != null && selectedLeaderboardsInGroup.size() == 1); } }); - groupDetailsTable.setSelectionModel(groupDetailsSelectionModel, groupDetailsTableSelectionColumn.getSelectionManager()); + groupDetailsTable.setSelectionModel(refreshableGroupDetailsSelectionModel, groupDetailsTableSelectionColumn.getSelectionManager()); groupDetailsPanel.add(groupDetailsTable); //Create details functionality @@ -466,12 +465,12 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements @Override public void onClick(ClickEvent event) { StringBuilder messageBuilder = new StringBuilder("Do you really want to remove the leaderboard groups:\n"); - for (LeaderboardGroupDTO group : groupsSelectionModel.getSelectedSet()) { + for (LeaderboardGroupDTO group : refreshableGroupsSelectionModel.getSelectedSet()) { messageBuilder.append("\n" + group.getName()); } if (Window.confirm(messageBuilder.toString())) { - removeLeaderboardGroups(groupsSelectionModel.getSelectedSet()); + removeLeaderboardGroups(refreshableGroupsSelectionModel.getSelectedSet()); } } }); @@ -570,14 +569,21 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements return group.getName(); } }; - groupsSelectionModel = new MultiSelectionModel(groupsKeyProvider); - groupsSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + refreshableGroupsSelectionModel = new RefreshableMultiSelectionModel<>(groupsKeyProvider, + new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(LeaderboardGroupDTO dto1, LeaderboardGroupDTO dto2) { + return dto1.getId().equals(dto2.getId()); + } + }); + refreshableGroupsSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { groupSelectionChanged(); } }); - groupsTable.setSelectionModel(groupsSelectionModel); + groupsTable.setSelectionModel(refreshableGroupsSelectionModel); groupsProvider.addDataDisplay(groupsTable); leaderboardGroupsContentPanel.add(groupsTable); @@ -593,18 +599,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements } groupsFilterablePanel.updateAll(availableLeaderboardGroups); - UUID id = getSelectedGroup().getId(); - if (id != null) { - groupsSelectionModel.clear(); - for (LeaderboardGroupDTO leaderboardGroup : availableLeaderboardGroups) { - if (id.equals(leaderboardGroup.getId())) { - groupsSelectionModel.setSelected(leaderboardGroup, true); - } else { - groupsSelectionModel.setSelected(leaderboardGroup, false); - } - } - groupSelectionChanged(); - } + refreshableGroupsSelectionModel.refreshSelectionModel(groups); } @Override @@ -624,7 +619,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements */ private void refreshLeaderboardsList() { final LeaderboardGroupDTO selectedGroup = getSelectedGroup(); - final Set selectedLeaderboards = leaderboardsSelectionModel.getSelectedSet(); + final Set selectedLeaderboards = refreshableLeaderboardsSelectionModel.getSelectedSet(); if (isSingleGroupSelected && selectedGroup != null) { sailingService.getLeaderboards(new MarkedAsyncCallback>( new AsyncCallback>() { @@ -644,9 +639,9 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements leaderboardsProvider.getList().removeAll(selectedGroup.leaderboards); leaderboardsFilterablePanel.getTextBox().setText(""); leaderboardsFilterablePanel.updateAll(availableLeaderboards); - leaderboardsSelectionModel.clear(); + refreshableLeaderboardsSelectionModel.clear(); for (StrippedLeaderboardDTO leaderboard : selectedLeaderboards) { - leaderboardsSelectionModel.setSelected(leaderboard, true); + refreshableLeaderboardsSelectionModel.setSelected(leaderboard, true); } } })); @@ -682,8 +677,8 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements public void onSuccess(LeaderboardGroupDTO newGroup) { availableLeaderboardGroups.add(newGroup); groupsFilterablePanel.updateAll(availableLeaderboardGroups); - groupsSelectionModel.clear(); - groupsSelectionModel.setSelected(newGroup, true); + refreshableGroupsSelectionModel.clear(); + refreshableGroupsSelectionModel.setSelected(newGroup, true); leaderboardGroupsRefresher.updateLeaderboardGroups(availableLeaderboardGroups, LeaderboardGroupConfigPanel.this); } })); @@ -713,7 +708,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements // if the leaderboard is selected, de-select equality/hashCode may change) and select again when done final boolean wasSelected = Util.equalsWithNull(groupToUpdate, getSelectedGroup()); if (wasSelected) { - groupsSelectionModel.setSelected(groupToUpdate, false); + refreshableGroupsSelectionModel.setSelected(groupToUpdate, false); } groupToUpdate.setName(updateDescriptor.getName()); groupToUpdate.description = updateDescriptor.getDescription(); @@ -727,7 +722,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements groupsProvider.getList().set(displayedIndex, groupToUpdate); } if (wasSelected) { - groupsSelectionModel.setSelected(groupToUpdate, true); + refreshableGroupsSelectionModel.setSelected(groupToUpdate, true); } } } @@ -807,13 +802,13 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements private void removeGroupFromTable(final LeaderboardGroupDTO group) { availableLeaderboardGroups.remove(group); groupsFilterablePanel.updateAll(availableLeaderboardGroups); - groupsSelectionModel.setSelected(group, false); + refreshableGroupsSelectionModel.setSelected(group, false); } private void groupSelectionChanged() { - isSingleGroupSelected = groupsSelectionModel.getSelectedSet().size() == 1; + isSingleGroupSelected = refreshableGroupsSelectionModel.getSelectedSet().size() == 1; final LeaderboardGroupDTO selectedGroup = getSelectedGroup(); removeButton.setEnabled(selectedGroup != null); splitPanel.setVisible(isSingleGroupSelected && selectedGroup != null); @@ -823,13 +818,13 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements descriptionTextArea.setText(selectedGroup.description); setDescriptionEditable(false); - groupDetailsSelectionModel.clear(); + refreshableGroupDetailsSelectionModel.refreshSelectionModel(availableLeaderboards); groupDetailsProvider.getList().clear(); groupDetailsProvider.getList().addAll(selectedGroup.leaderboards); groupDetailsProvider.refresh(); // Tell leaderboards filter panel to use all available leaderboards except the leaderboards of the group from the list - leaderboardsSelectionModel.clear(); + refreshableLeaderboardsSelectionModel.clear(); ArrayList allLeaderboardsExceptThoseOfSelectedLeaderboardGroup = new ArrayList<>(availableLeaderboards); allLeaderboardsExceptThoseOfSelectedLeaderboardGroup.removeAll(selectedGroup.leaderboards); leaderboardsFilterablePanel.updateAll(allLeaderboardsExceptThoseOfSelectedLeaderboardGroup); @@ -843,12 +838,12 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements private void moveToLeaderboards() { LeaderboardGroupDTO selectedGroup = getSelectedGroup(); - Set selectedLeaderboards = groupDetailsSelectionModel.getSelectedSet(); + Set selectedLeaderboards = refreshableGroupDetailsSelectionModel.getSelectedSet(); if (isSingleGroupSelected && selectedGroup != null && selectedLeaderboards != null && selectedLeaderboards.size() > 0) { for (StrippedLeaderboardDTO leaderboard : selectedLeaderboards) { selectedGroup.leaderboards.remove(leaderboard); groupDetailsProvider.getList().remove(leaderboard); - groupDetailsSelectionModel.setSelected(leaderboard, false); + refreshableGroupDetailsSelectionModel.setSelected(leaderboard, false); leaderboardsProvider.getList().add(leaderboard); } updateGroup(selectedGroup); @@ -860,7 +855,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements private void moveToGroup() { final LeaderboardGroupDTO selectedGroup = getSelectedGroup(); - ArrayList selectedLeaderboards = new ArrayList(leaderboardsSelectionModel.getSelectedSet()); + ArrayList selectedLeaderboards = new ArrayList(refreshableLeaderboardsSelectionModel.getSelectedSet()); if (isSingleGroupSelected && selectedGroup != null && selectedLeaderboards != null && !selectedLeaderboards.isEmpty()) { Collections.sort(selectedLeaderboards, new Comparator() { @Override @@ -874,7 +869,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements selectedGroup.leaderboards.add(leaderboard); groupDetailsProvider.getList().add(leaderboard); leaderboardsProvider.getList().remove(leaderboard); - leaderboardsSelectionModel.setSelected(leaderboard, false); + refreshableLeaderboardsSelectionModel.setSelected(leaderboard, false); } } updateGroup(selectedGroup); @@ -883,7 +878,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements private void moveLeaderboardInGroupUp() { LeaderboardGroupDTO selectedGroup = getSelectedGroup(); - Set selectedLeaderboards = groupDetailsSelectionModel.getSelectedSet(); + Set selectedLeaderboards = refreshableGroupDetailsSelectionModel.getSelectedSet(); if (isSingleGroupSelected && selectedGroup != null && selectedLeaderboards != null && selectedLeaderboards.size() == 1) { StrippedLeaderboardDTO selectedLeaderboard = selectedLeaderboards.iterator().next(); moveLeaderboardInGroup(selectedGroup, selectedLeaderboard, -1); @@ -892,7 +887,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements private void moveLeaderboardInGroupDown() { LeaderboardGroupDTO selectedGroup = getSelectedGroup(); - Set selectedLeaderboards = groupDetailsSelectionModel.getSelectedSet(); + Set selectedLeaderboards = refreshableGroupDetailsSelectionModel.getSelectedSet(); if (isSingleGroupSelected && selectedGroup != null && selectedLeaderboards != null && selectedLeaderboards.size() == 1) { StrippedLeaderboardDTO selectedLeaderboard = selectedLeaderboards.iterator().next(); moveLeaderboardInGroup(selectedGroup, selectedLeaderboard, 1); @@ -946,6 +941,6 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements } private LeaderboardGroupDTO getSelectedGroup() { - return groupsSelectionModel.getSelectedSet().isEmpty() ? null : groupsSelectionModel.getSelectedSet().iterator().next(); + return refreshableGroupsSelectionModel.getSelectedSet().isEmpty() ? null : refreshableGroupsSelectionModel.getSelectedSet().iterator().next(); } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 4ee2b99fe18..4e544926fd0 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -4,6 +4,7 @@ import java.util.Set; import com.google.gwt.view.client.MultiSelectionModel; import com.google.gwt.view.client.ProvidesKey; +import com.google.gwt.view.client.SelectionChangeEvent; public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel { private final EntityIdentityComparator comp; @@ -44,8 +45,9 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im } setSelected(it, isSelected); } - setEventScheduled(true); - fireSelectionChangeEvent(); + //setEventScheduled(true); + //fireSelectionChangeEvent(); + SelectionChangeEvent.fire(this); } } } \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index 9907c58c899..268d9e78e6f 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -1,6 +1,7 @@ package com.sap.sse.gwt.client.celltable; import com.google.gwt.view.client.ProvidesKey; +import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SingleSelectionModel; public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel { @@ -38,7 +39,8 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel } } } - setEventScheduled(true); - fireSelectionChangeEvent(); + //setEventScheduled(true); + //fireSelectionChangeEvent(); + SelectionChangeEvent.fire(this); } } \ No newline at end of file From 6983643e588684b67ddedebf910cd9a52ba2d28c Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Mon, 30 Nov 2015 10:41:54 +0100 Subject: [PATCH 061/306] Adjusted TimeSlider tick label rendering to avoid overlapping --- .../client/controls/slider/TimeSlider.java | 24 +- .../controls/slider/TimeTicksCalculator.java | 493 +++++++++--------- 2 files changed, 266 insertions(+), 251 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java index 99dafe676bc..8383b5a6c60 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java @@ -26,7 +26,9 @@ public class TimeSlider extends SliderBar { private List calculatedTimeTicks; private boolean isZoomed; - + + private int visibleLabelsInterval = 1; + private final int TICKCOUNT = 10; public TimeSlider() { @@ -84,7 +86,7 @@ public class TimeSlider extends SliderBar { int tickLeftOffset = lineLeftOffset + (int) pos - (tickWidth / 2); tickLeftOffset = Math.min(tickLeftOffset, lineLeftOffset + lineWidth - tickWidth); tick.getStyle().setLeft(tickLeftOffset, Unit.PX); - tick.getStyle().setVisibility(Visibility.VISIBLE); + tick.getStyle().setVisibility(isTickInVisibleRange(tickPosition) ? Visibility.VISIBLE : Visibility.HIDDEN); } } @@ -106,7 +108,7 @@ public class TimeSlider extends SliderBar { int lineWidth = lineElement.getOffsetWidth(); // Create the labels or make them visible Double previousValue = null; - for (int i = 0; i < calculatedTimeTicks.size(); i++) { + for (int i = 0, ticksInVisibleRange = 0; i < calculatedTimeTicks.size(); i++) { TickPosition tickPosition = calculatedTimeTicks.get(i); Element label = null; if (i < tickLabelElements.size()) { @@ -142,7 +144,8 @@ public class TimeSlider extends SliderBar { labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); labelLeftOffset = Math.max(labelLeftOffset, lineLeftOffset); label.getStyle().setLeft(labelLeftOffset, Unit.PX); - label.getStyle().setVisibility(Visibility.VISIBLE); + boolean visible = isTickInVisibleRange(tickPosition) && ticksInVisibleRange++ % visibleLabelsInterval == 0; + label.getStyle().setVisibility(visible ? Visibility.VISIBLE : Visibility.HIDDEN); } previousValue = value; } @@ -152,6 +155,11 @@ public class TimeSlider extends SliderBar { tickLabelElements.get(i).getStyle().setDisplay(Display.NONE); } } + + private boolean isTickInVisibleRange(TickPosition tickPosition) { + long tickValue = tickPosition.getPosition().getTime(); + return tickValue >= minValue && tickValue <= maxValue; + } public void clearMarkersAndLabelsAndTicks() { clearMarkers(); @@ -245,4 +253,10 @@ public class TimeSlider extends SliderBar { public void setZoomed(boolean isZoomed) { this.isZoomed = isZoomed; } -} + + @Override + public void onResize() { + visibleLabelsInterval = (TICKCOUNT / Math.max(getOffsetWidth() / 35, 1)) + 1; + super.onResize(); + } +} \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java index 8a28bd12fb2..735989a2776 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java @@ -1,246 +1,247 @@ -package com.sap.sse.gwt.client.controls.slider; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -/** - * This calculator emulates the way the HighCharts charting library is computing the chart ticks - * in case of a x-axis based on time values. - */ -public class TimeTicksCalculator { - - public TimeTicksCalculator() { - } - - /** - * Get a normalized tick interval for dates. Returns a configuration object with unit range (interval), count and - * name. Used to prepare data for getTimeTicks. Previously this logic was part of getTimeTicks, but as getTimeTicks - * now runs of segments in stock charts, the normalizing logic was extracted in order to prevent it for running over - * again for each segment having the same interval. - */ - public NormalizedInterval normalizeTimeTickInterval(long tickInterval) { - TimeUnits[] units = TimeUnits.values(); - - TimeUnits unit = units[units.length - 1]; // default unit is years - long interval = unit.unitInMs; - int[] multiples = unit.allowedMultiples; - int count; - - // loop through the units to find the one that best fits the tickInterval - for (int i = 0; i < units.length; i++) { - unit = units[i]; - interval = unit.unitInMs; - multiples = unit.allowedMultiples; - - if (i + 1 < units.length) { - // lessThan is in the middle between the highest multiple and the next unit. - long lessThan = (interval * multiples[multiples.length - 1] + units[i + 1].unitInMs) / 2; - - // break and keep the current unit - if (tickInterval <= lessThan) { - break; - } - } - } - - // prevent 2.5 years intervals, though 25, 250 etc. are allowed - if (interval == TimeUnits.YEAR.unitInMs && tickInterval < 5 * interval) { - multiples = new int[] { 1, 2, 5 }; - } - - // get the count - count = (int) normalizeTickInterval(tickInterval / interval, multiples, 1); - - return new NormalizedInterval(unit.name(), interval, count); - } - - /** - * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 - * - * @param {Number} interval - * @param {Array} multiples - * @param {Number} magnitude - * @param {Object} options - */ - public long normalizeTickInterval(long interval, int[] multiples, int magnitude) { - long normalized; - - // round to a tenfold of 1, 2, 2.5 or 5 - normalized = interval / magnitude; - - // normalize the interval to the nearest multiple - for (int i = 0; i < multiples.length; i++) { - interval = multiples[i]; - int addedValue = i + 1 < multiples.length ? multiples[i + 1] : multiples[i]; - if (normalized <= (multiples[i] + addedValue) / 2) { - break; - } - } - - // multiply back to the correct magnitude - interval *= magnitude; - - return interval; - } - - private long makeTime(int year, int month) { - return makeTime(year, month, 1, 0, 0, 0); - } - - private long makeTime(int year, int month, int date) { - return makeTime(year, month, date, 0, 0, 0); - } - - @SuppressWarnings("deprecation") - private long makeTime(int year, int month, int date, int hours, int minutes, int seconds) { - return new Date(year, month, date, hours, minutes, seconds).getTime(); - } - - /** - * Set the tick positions to a time unit that makes sense, for example on the first of each month or on every - * Monday. Return an array with the time positions. Used in datetime axes as well as for grouping data on a datetime - * axis. - * - * @param {Object} normalizedInterval The interval in axis values (ms) and the count - * @param {Number} min The minimum in axis values - * @param {Number} max The maximum in axis values - * @param {Number} startOfWeek - */ - @SuppressWarnings("deprecation") - public List calculateTimeTicks(NormalizedInterval normalizedInterval, long min, long max, - long startOfWeek) { - List tickPositions = new ArrayList(); - int i; - boolean useUTC = true; - int minYear = 0; // used in months and years as a basis for Date.UTC() - Date minDate = new Date(min); - long interval = normalizedInterval.unitRange; - int count = (int) normalizedInterval.count; - - - if (interval >= TimeUnits.SECOND.unitInMs) { // second - minDate.setTime(min - (min % 1000)); - minDate.setSeconds(interval >= TimeUnits.MINUTE.unitInMs ? 0 : count - * Math.round(minDate.getSeconds() / count)); - } - - if (interval >= TimeUnits.MINUTE.unitInMs) { // minute - minDate.setMinutes(interval >= TimeUnits.HOUR.unitInMs ? 0 : count - * Math.round(minDate.getMinutes() / count)); - } - - if (interval >= TimeUnits.HOUR.unitInMs) { // hour - minDate.setHours(interval >= TimeUnits.DAY.unitInMs ? 0 : count * Math.round(minDate.getHours() / count)); - } - - if (interval >= TimeUnits.DAY.unitInMs) { // day - minDate.setDate(interval >= TimeUnits.MONTH.unitInMs ? 1 : count * Math.round(minDate.getDate() / count)); - } - - if (interval >= TimeUnits.MONTH.unitInMs) { // month - minDate.setMonth(interval >= TimeUnits.YEAR.unitInMs ? 0 : count * Math.round(minDate.getMonth() + 1 / count) - 1); - minYear = minDate.getYear() + 1900; - } - - if (interval >= TimeUnits.YEAR.unitInMs) { // year - minYear -= minYear % count; - minDate.setYear(minYear - 1900); - } - - // week is a special case that runs outside the hierarchy - if (interval == TimeUnits.WEEK.unitInMs) { - // get start of current week, independent of count - long value = minDate.getDate() - minDate.getDay() + startOfWeek; - minDate.setDate((int) value); - } - - // get tick positions - i = 1; - minYear = minDate.getYear() + 1900; - int minMonth = minDate.getMonth() + 1; - int minDateDate = minDate.getDate(); - long time = minDate.getTime(); - - // iterate and add tick positions at appropriate values - while (time < max) { - if(time >= min) - tickPositions.add(new TickPosition(time)); - - // if the interval is years, use Date.UTC to increase years - if (interval == TimeUnits.YEAR.unitInMs) { - time = makeTime(minYear + i * count, 0); - - // if the interval is months, use Date.UTC to increase months - } else if (interval == TimeUnits.MONTH.unitInMs) { - time = makeTime(minYear, minMonth + i * count); - - // if we're using global time, the interval is not fixed as it jumps - // one hour at the DST crossover - } else if (!useUTC && (interval == TimeUnits.DAY.unitInMs || interval == TimeUnits.WEEK.unitInMs)) { - time = makeTime(minYear, minMonth, minDateDate + i * count * (interval == TimeUnits.DAY.unitInMs ? 1 : 7)); - - // else, the interval is fixed and we use simple addition - } else { - time += interval * count; - } - - i++; - } - - // push the last time - if(time <= max) - tickPositions.add(new TickPosition(time)); - - return tickPositions; - } - - public class NormalizedInterval { - public String unitName; - public long unitRange; - public int count; - - public NormalizedInterval(String unitName, long unitRange, int count) { - this.unitName = unitName; - this.unitRange = unitRange; - this.count = count; - } - - @Override - public String toString() { - return "NormalizedInterval [unitName=" + unitName + ", unitRange=" + unitRange + ", count=" + count + "]"; - } - } - - public class TickPosition { - private Date position; - - public TickPosition(long time) { - this.position = new Date(time); - } - - public Date getPosition() { - return position; - } - } - - enum TimeUnits { - MILLISECOND(1, new int[] { 1, 2, 5, 10, 20, 25, 50, 100, 200, 500 }), - SECOND(1000, new int[] { 1, 2, 5, 10, 15, 30 }), - MINUTE(60 * 1000, new int[] { 1, 2, 5, 10, 15, 30 }), - HOUR(60 * 60 * 1000, new int[] { 1, 2, 3, 4, 6, 8, 12 }), - DAY(24 * 3600000l, new int[] { 1, 2 }), - WEEK(7 * 24 * 3600000l, new int[] { 1, 2 }), - MONTH(30 * 24 * 3600000l, new int[] { 1, 2, 3, 4, 6 }), - YEAR(31556952000l, new int[] {}); - - int[] allowedMultiples; - - long unitInMs; - - TimeUnits(long unitInMs, int[] allowedMultiples) { - this.unitInMs = unitInMs; - this.allowedMultiples = allowedMultiples; - } - } -} +package com.sap.sse.gwt.client.controls.slider; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * This calculator emulates the way the HighCharts charting library is computing the chart ticks + * in case of a x-axis based on time values. + */ +public class TimeTicksCalculator { + + public TimeTicksCalculator() { + } + + /** + * Get a normalized tick interval for dates. Returns a configuration object with unit range (interval), count and + * name. Used to prepare data for getTimeTicks. Previously this logic was part of getTimeTicks, but as getTimeTicks + * now runs of segments in stock charts, the normalizing logic was extracted in order to prevent it for running over + * again for each segment having the same interval. + */ + public NormalizedInterval normalizeTimeTickInterval(long tickInterval) { + TimeUnits[] units = TimeUnits.values(); + + TimeUnits unit = units[units.length - 1]; // default unit is years + long interval = unit.unitInMs; + int[] multiples = unit.allowedMultiples; + int count; + + // loop through the units to find the one that best fits the tickInterval + for (int i = 0; i < units.length; i++) { + unit = units[i]; + interval = unit.unitInMs; + multiples = unit.allowedMultiples; + + if (i + 1 < units.length) { + // lessThan is in the middle between the highest multiple and the next unit. + double lessThan = (interval * multiples[multiples.length - 1] + units[i + 1].unitInMs) / 2; + + // break and keep the current unit + if (tickInterval <= lessThan) { + break; + } + } + } + + // prevent 2.5 years intervals, though 25, 250 etc. are allowed + if (interval == TimeUnits.YEAR.unitInMs && tickInterval < 5 * interval) { + multiples = new int[] { 1, 2, 5 }; + } + + // get the count + double temp = ((double) tickInterval) / ((double) interval); + count = (int) normalizeTickInterval(temp, multiples, 1); + + return new NormalizedInterval(unit.name(), interval, count); + } + + /** + * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 + * + * @param {Number} interval + * @param {Array} multiples + * @param {Number} magnitude + * @param {Object} options + */ + public long normalizeTickInterval(double interval, int[] multiples, int magnitude) { + double normalized; + + // round to a tenfold of 1, 2, 2.5 or 5 + normalized = interval / magnitude; + + // normalize the interval to the nearest multiple + for (int i = 0; i < multiples.length; i++) { + interval = multiples[i]; + int addedValue = i + 1 < multiples.length ? multiples[i + 1] : multiples[i]; + if (normalized <= (multiples[i] + addedValue) / 2) { + break; + } + } + + // multiply back to the correct magnitude + interval *= magnitude; + + return Math.round(interval); + } + + private long makeTime(int year, int month) { + return makeTime(year, month, 1, 0, 0, 0); + } + + private long makeTime(int year, int month, int date) { + return makeTime(year, month, date, 0, 0, 0); + } + + @SuppressWarnings("deprecation") + private long makeTime(int year, int month, int date, int hours, int minutes, int seconds) { + return new Date(year, month, date, hours, minutes, seconds).getTime(); + } + + /** + * Set the tick positions to a time unit that makes sense, for example on the first of each month or on every + * Monday. Return an array with the time positions. Used in datetime axes as well as for grouping data on a datetime + * axis. + * + * @param {Object} normalizedInterval The interval in axis values (ms) and the count + * @param {Number} min The minimum in axis values + * @param {Number} max The maximum in axis values + * @param {Number} startOfWeek + */ + @SuppressWarnings("deprecation") + public List calculateTimeTicks(NormalizedInterval normalizedInterval, long min, long max, + long startOfWeek) { + List tickPositions = new ArrayList(); + int i; + boolean useUTC = true; + int minYear = 0; // used in months and years as a basis for Date.UTC() + Date minDate = new Date(min); + long interval = normalizedInterval.unitRange; + int count = (int) normalizedInterval.count; + + + if (interval >= TimeUnits.SECOND.unitInMs) { // second + minDate.setTime(min - (min % 1000)); + minDate.setSeconds(interval >= TimeUnits.MINUTE.unitInMs ? 0 : count + * Math.round(minDate.getSeconds() / count)); + } + + if (interval >= TimeUnits.MINUTE.unitInMs) { // minute + minDate.setMinutes(interval >= TimeUnits.HOUR.unitInMs ? 0 : count + * Math.round(minDate.getMinutes() / count)); + } + + if (interval >= TimeUnits.HOUR.unitInMs) { // hour + minDate.setHours(interval >= TimeUnits.DAY.unitInMs ? 0 : count * Math.round(minDate.getHours() / count)); + } + + if (interval >= TimeUnits.DAY.unitInMs) { // day + minDate.setDate(interval >= TimeUnits.MONTH.unitInMs ? 1 : count * Math.round(minDate.getDate() / count)); + } + + if (interval >= TimeUnits.MONTH.unitInMs) { // month + minDate.setMonth(interval >= TimeUnits.YEAR.unitInMs ? 0 : count * Math.round(minDate.getMonth() + 1 / count) - 1); + minYear = minDate.getYear() + 1900; + } + + if (interval >= TimeUnits.YEAR.unitInMs) { // year + minYear -= minYear % count; + minDate.setYear(minYear - 1900); + } + + // week is a special case that runs outside the hierarchy + if (interval == TimeUnits.WEEK.unitInMs) { + // get start of current week, independent of count + long value = minDate.getDate() - minDate.getDay() + startOfWeek; + minDate.setDate((int) value); + } + + // get tick positions + i = 1; + minYear = minDate.getYear() + 1900; + int minMonth = minDate.getMonth() + 1; + int minDateDate = minDate.getDate(); + long time = minDate.getTime(); + + // iterate and add tick positions at appropriate values + while (time < max) { + if(time >= min) + tickPositions.add(new TickPosition(time)); + + // if the interval is years, use Date.UTC to increase years + if (interval == TimeUnits.YEAR.unitInMs) { + time = makeTime(minYear + i * count, 0); + + // if the interval is months, use Date.UTC to increase months + } else if (interval == TimeUnits.MONTH.unitInMs) { + time = makeTime(minYear, minMonth + i * count); + + // if we're using global time, the interval is not fixed as it jumps + // one hour at the DST crossover + } else if (!useUTC && (interval == TimeUnits.DAY.unitInMs || interval == TimeUnits.WEEK.unitInMs)) { + time = makeTime(minYear, minMonth, minDateDate + i * count * (interval == TimeUnits.DAY.unitInMs ? 1 : 7)); + + // else, the interval is fixed and we use simple addition + } else { + time += interval * count; + } + + i++; + } + + // push the last time + if(time <= max) + tickPositions.add(new TickPosition(time)); + + return tickPositions; + } + + public class NormalizedInterval { + public String unitName; + public long unitRange; + public int count; + + public NormalizedInterval(String unitName, long unitRange, int count) { + this.unitName = unitName; + this.unitRange = unitRange; + this.count = count; + } + + @Override + public String toString() { + return "NormalizedInterval [unitName=" + unitName + ", unitRange=" + unitRange + ", count=" + count + "]"; + } + } + + public class TickPosition { + private Date position; + + public TickPosition(long time) { + this.position = new Date(time); + } + + public Date getPosition() { + return position; + } + } + + enum TimeUnits { + MILLISECOND(1l, 1, 2, 5, 10, 20, 25, 50, 100, 200, 500), + SECOND(1000l, 1, 2, 5, 10, 15, 30), + MINUTE(60 * 1000l, 1, 2, 5, 10, 15, 30), + HOUR(60 * 60 * 1000l, 1, 2, 3, 4, 6, 8, 12), + DAY(24 * 3600000l, 1, 2), + WEEK(7 * 24 * 3600000l, 1, 2), + MONTH(30 * 24 * 3600000l, 1, 2, 3, 4, 6), + YEAR(31556952000l); + + int[] allowedMultiples; + + long unitInMs; + + TimeUnits(long unitInMs, int... allowedMultiples) { + this.unitInMs = unitInMs; + this.allowedMultiples = allowedMultiples; + } + } +} From 638cd5a871885e9b8e3f1a7b360aa69761cb7460 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Mon, 30 Nov 2015 11:02:21 +0100 Subject: [PATCH 062/306] Adjusted SliderBar marker label rendering to ensure bigger font size if marker are no longer "too close" when increasing window size --- .../gwt/client/controls/slider/SliderBar.java | 2512 ++++++++--------- 1 file changed, 1256 insertions(+), 1256 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java index 93ffbc08988..3f31188524e 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java @@ -1,1256 +1,1256 @@ -package com.sap.sse.gwt.client.controls.slider; - -/* - * Copyright 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -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.Element; -import com.google.gwt.dom.client.Style.Display; -import com.google.gwt.dom.client.Style.Position; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.dom.client.Style.Visibility; -import com.google.gwt.event.dom.client.KeyCodes; -import com.google.gwt.event.logical.shared.HasValueChangeHandlers; -import com.google.gwt.event.logical.shared.ValueChangeEvent; -import com.google.gwt.event.logical.shared.ValueChangeHandler; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.resources.client.ClientBundle; -import com.google.gwt.resources.client.CssResource; -import com.google.gwt.resources.client.CssResource.NotStrict; -import com.google.gwt.resources.client.ImageResource; -import com.google.gwt.user.client.DOM; -import com.google.gwt.user.client.Event; -import com.google.gwt.user.client.Timer; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.FocusPanel; -import com.google.gwt.user.client.ui.HasValue; -import com.google.gwt.user.client.ui.Image; -import com.google.gwt.user.client.ui.RequiresResize; -import com.sap.sse.common.Util; - -/** - * A widget that allows the user to select a value within a range of possible values using a sliding bar that responds - * to mouse events. - * - *

    Keyboard Events

    - *

    - * SliderBar listens for the following key events. Holding down a key will repeat the action until the key is released. - *

      - *
    • left arrow - shift left one step
    • - *
    • right arrow - shift right one step
    • - *
    • ctrl+left arrow - jump left 10% of the distance
    • - *
    • ctrl+right arrow - jump right 10% of the distance
    • - *
    • home - jump to min value
    • - *
    • end - jump to max value
    • - *
    • space - jump to middle value
    • - *
    - *

    - * - *

    CSS Style Rules

    - *
      - *
    • .gwt-SliderBar-shell { primary style }
    • - *
    • .gwt-SliderBar-shell-focused { primary style when focused }
    • - *
    • .gwt-SliderBar-shell gwt-SliderBar-line { the line that the knob moves along }
    • - *
    • .gwt-SliderBar-shell gwt-SliderBar-line-sliding { the line that the knob moves along when sliding }
    • - *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob { the sliding knob }
    • - *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob-sliding { the sliding knob when sliding }
    • - *
    • - * .gwt-SliderBar-shell .gwt-SliderBar-tick { the ticks along the line }
    • - *
    • .gwt-SliderBar-shell .gwt-SliderBar-ticklabel { the text labels along the line }
    • - *
    - */ -public class SliderBar extends FocusPanel implements RequiresResize, HasValue, HasValueChangeHandlers { - /** - * A formatter used to format the labels displayed in the widget. - */ - public static interface LabelFormatter { - /** - * Generate the text to display in each label based on the label's value. - * - * Override this method to change the text displayed within the SliderBar. - * - * @param slider - * the Slider bar - * @param value - * the value the label displays - * @return the text to display for the label - */ - String formatLabel(SliderBar slider, Double value, Double previousValue); - } - - /** - * A {@link ClientBundle} that provides images for {@link SliderBar}. - */ - public static interface SliderBarImages extends ClientBundle { - public static final SliderBarImages INSTANCE = GWT.create(SliderBarImages.class); - - /** - * An image used for the sliding knob. - * - * @return a prototype of this image - */ - @Source("slider_blue.png") - ImageResource slider(); - - /** - * An image used for the disabled sliding knob. - * - * @return a prototype of this image - */ - @Source("slider_blue.png") - ImageResource sliderDisabled(); - - /** - * An image used for the sliding knob while sliding. - * - * @return a prototype of this image - */ - @Source("slider_blue.png") - ImageResource sliderSliding(); - - @NotStrict - @Source("SliderBar.css") - CssResource sliderBarCss(); - } - - /** - * The timer used to continue to shift the knob as the user holds down one of the left/right arrow keys. Only IE - * auto-repeats, so we just keep catching the events. - */ - private class KeyTimer extends Timer { - /** - * A bit indicating that this is the first run. - */ - private boolean firstRun = true; - - /** - * The delay between shifts, which shortens as the user holds down the button. - */ - private int repeatDelay = 30; - - /** - * A bit indicating whether we are shifting to a higher or lower value. - */ - private boolean shiftRight = false; - - /** - * The number of steps to shift with each press. - */ - private int multiplier = 1; - - /** - * This method will be called when a timer fires. Override it to implement the timer's logic. - */ - @Override - public void run() { - // Highlight the knob on first run - if (firstRun) { - firstRun = false; - startSliding(true, false); - } - - // Slide the slider bar - if (shiftRight) { - setCurrentValue(curValue + multiplier * stepSize); - } else { - setCurrentValue(curValue - multiplier * stepSize); - } - - // Repeat this timer until cancelled by keyup event - schedule(repeatDelay); - } - - /** - * Schedules a timer to elapse in the future. - * - * @param delayMillis - * how long to wait before the timer elapses, in milliseconds - * @param shiftRight - * whether to shift up or not - * @param multiplier - * the number of steps to shift - */ - public void schedule(int delayMillis, boolean shiftRight, int multiplier) { - firstRun = true; - this.shiftRight = shiftRight; - this.multiplier = multiplier; - super.schedule(delayMillis); - } - } - - /** - * The current value. - */ - protected Double curValue; - - /** - * The knob that slides across the line. - */ - protected Image knobImage = new Image(); - - /** - * The timer used to continue to shift the knob if the user holds down a key. - */ - private KeyTimer keyTimer = new KeyTimer(); - - /** - * The elements used to display labels above the ticks. - */ - protected List tickLabelElements = new ArrayList(); - - /** - * The elements used to display the marker labels. - */ - protected List markerLabelElements = new ArrayList(); - - /** - * The formatter used to generate label text. - */ - protected LabelFormatter tickLabelFormatter; - - /** - * The line that the knob moves over. - */ - protected Element lineElement; - - /** - * The offset between the edge of the shell and the line. - */ - protected int lineLeftOffset = 0; - - /** - * The maximum slider value. - */ - protected Double maxValue; - - /** - * The minimum slider value. - */ - protected Double minValue; - - /** - * The number of labels to show. - */ - private int numTickLabels = 0; - - /** - * The number of tick marks to show. - */ - protected int numTicks = 0; - - /** - * A bit indicating whether or not we are currently sliding the slider bar due to keyboard events. - */ - private boolean slidingKeyboard = false; - - /** - * A bit indicating whether or not we are currently sliding the slider bar due to mouse events. - */ - private boolean slidingMouse = false; - - /** - * A bit indicating whether or not the slider is enabled - */ - protected boolean enabled = true; - - /** - * The images used with the sliding bar. - */ - private SliderBarImages images; - - /** - * The size of the increments between knob positions. - */ - protected double stepSize; - - /** - * The elements used to display tick marks, which are the vertical lines along the slider bar. - */ - protected List tickElements = new ArrayList(); - - /** - * The elements used to display additional markers on the slider bar. - */ - protected List markerElements = new ArrayList(); - - private List markers = new ArrayList(); - - private class Marker { - String name; - Double position; - - public Marker(String name, Double position) { - super(); - this.name = name; - this.position = position; - } - } - - /** - * Create a slider bar. - */ - public SliderBar() { - this(null, null, null); - } - - /** - * Create a slider bar. - * - * @param minValue - * the minimum value in the range - * @param maxValue - * the maximum value in the range - */ - public SliderBar(double minValue, double maxValue) { - this(minValue, maxValue, null); - } - - /** - * Create a slider bar. - * - * @param minValue - * the minimum value in the range - * @param maxValue - * the maximum value in the range - * @param tickLabelFormatter - * the label formatter - */ - public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter) { - this(minValue, maxValue, tickLabelFormatter, SliderBarImages.INSTANCE); - } - - /** - * Create a slider bar. - * - * @param minValue - * the minimum value in the range - * @param maxValue - * the maximum value in the range - * @param tickLabelFormatter - * the label formatter - * @param images - * the images to use for the slider - */ - public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter, SliderBarImages images) { - super(); - images.sliderBarCss().ensureInjected(); - this.minValue = minValue; - this.maxValue = maxValue; - this.images = images; - setLabelFormatter(tickLabelFormatter); - - // Create the outer shell - getElement().getStyle().setPosition(Position.RELATIVE); - setStyleName("gwt-SliderBar-shell"); - - // Create the line - lineElement = DOM.createDiv(); - DOM.appendChild(getElement(), lineElement); - lineElement.getStyle().setPosition(Position.ABSOLUTE); - lineElement.setPropertyString("className", "gwt-SliderBar-line"); - - // Create the knob - knobImage.setResource(images.slider()); - Element knobElement = knobImage.getElement(); - DOM.appendChild(getElement(), knobElement); - knobElement.getStyle().setPosition(Position.ABSOLUTE); - knobElement.setPropertyString("className", "gwt-SliderBar-knob"); - - sinkEvents(Event.MOUSEEVENTS | Event.KEYEVENTS | Event.FOCUSEVENTS); - - // workaround to render properly when parent Widget does not - // implement ProvidesResize since DOM doesn't provide element - // height and width until onModuleLoad() finishes. - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - onResize(); - } - }); - } - - public boolean isMinMaxInitialized() { - return minValue != null && maxValue != null; - } - - public HandlerRegistration addValueChangeHandler(ValueChangeHandler handler) { - return addHandler(handler, ValueChangeEvent.getType()); - } - - /** - * Return the current value. - * - * @return the current value - */ - public Double getCurrentValue() { - return curValue; - } - - /** - * Return the label formatter. - * - * @return the label formatter - */ - public LabelFormatter getLabelFormatter() { - return tickLabelFormatter; - } - - /** - * Return the max value. - * - * @return the max value - */ - public Double getMaxValue() { - return maxValue; - } - - /** - * Return the minimum value. - * - * @return the minimum value - */ - public Double getMinValue() { - return minValue; - } - - /** - * Return the number of labels. - * - * @return the number of labels - */ - public int getNumLabels() { - return numTickLabels; - } - - /** - * Return the number of ticks. - * - * @return the number of ticks - */ - public int getNumTicks() { - return numTicks; - } - - /** - * Return the step size. - * - * @return the step size - */ - public double getStepSize() { - return stepSize; - } - - /** - * Return the total range between the minimum and maximum values. - * - * @return the total range - */ - public double getTotalRange() { - if (minValue == null || maxValue == null || minValue > maxValue) { - return 0; - } else { - return maxValue - minValue; - } - } - - public Double getValue() { - return curValue; - } - - /** - * @return Gets whether this widget is enabled - */ - public boolean isEnabled() { - return enabled; - } - - /** - * Listen for events that will move the knob. - * - * @param event - * the event that occurred - */ - @Override - public void onBrowserEvent(Event event) { - super.onBrowserEvent(event); - if (enabled) { - switch (DOM.eventGetType(event)) { - // Unhighlight and cancel keyboard events - case Event.ONBLUR: - keyTimer.cancel(); - if (slidingMouse) { - DOM.releaseCapture(getElement()); - slidingMouse = false; - slideKnob(event); - stopSliding(true, true); - } else if (slidingKeyboard) { - slidingKeyboard = false; - stopSliding(true, true); - } - unhighlight(); - break; - - // Highlight on focus - case Event.ONFOCUS: - highlight(); - break; - - // Mousewheel events - case Event.ONMOUSEWHEEL: - int velocityY = event.getMouseWheelVelocityY(); - event.preventDefault(); - if (velocityY > 0) { - shiftRight(1); - } else { - shiftLeft(1); - } - break; - - // Shift left or right on key press - case Event.ONKEYDOWN: - if (!slidingKeyboard) { - int multiplier = 1; - if (event.getCtrlKey()) { - multiplier = (int) (getTotalRange() / stepSize / 10); - } - - switch (event.getKeyCode()) { - case KeyCodes.KEY_HOME: - event.preventDefault(); - setCurrentValue(minValue); - break; - case KeyCodes.KEY_END: - event.preventDefault(); - setCurrentValue(maxValue); - break; - case KeyCodes.KEY_LEFT: - event.preventDefault(); - slidingKeyboard = true; - startSliding(false, true); - shiftLeft(multiplier); - keyTimer.schedule(400, false, multiplier); - break; - case KeyCodes.KEY_RIGHT: - event.preventDefault(); - slidingKeyboard = true; - startSliding(false, true); - shiftRight(multiplier); - keyTimer.schedule(400, true, multiplier); - break; - case 32: - event.preventDefault(); - setCurrentValue(minValue + getTotalRange() / 2); - break; - } - } - break; - // Stop shifting on key up - case Event.ONKEYUP: - keyTimer.cancel(); - if (slidingKeyboard) { - slidingKeyboard = false; - stopSliding(true, true); - } - break; - - // Mouse Events - case Event.ONMOUSEDOWN: - setFocus(true); - slidingMouse = true; - DOM.setCapture(getElement()); - startSliding(true, true); - event.preventDefault(); - slideKnob(event); - break; - case Event.ONMOUSEUP: - if (slidingMouse) { - DOM.releaseCapture(getElement()); - slidingMouse = false; - slideKnob(event); - stopSliding(true, true); - } - break; - case Event.ONMOUSEMOVE: - if (slidingMouse) { - slideKnob(event); - } - break; - } - } - } - - /** - * This method is called when the dimensions of the parent element change. Subclasses should override this method as - * needed. - * - * @param width - * the new client width of the element - * @param height - * the new client height of the element - */ - public void onResize(int width, int height) { - // Center the line in the shell - int lineWidth = lineElement.getOffsetWidth(); - lineLeftOffset = (width / 2) - (lineWidth / 2); - lineElement.getStyle().setLeft(lineLeftOffset, Unit.PX); - - // Draw the other components - drawTickLabels(); - drawTicks(); - drawMarkers(); - drawMarkerLabels(); - drawKnob(); - } - - /** - * Redraw the progress bar when something changes the layout. - */ - public void redraw() { - if (isAttached()) { - int width = getElement().getClientWidth(); - int height = getElement().getClientHeight(); - onResize(width, height); - } - } - - /** - * Set the current value and fire the onValueChange event. - * - * @param curValue - * the current value - */ - public synchronized void setCurrentValue(Double curValue) { - setCurrentValue(curValue, true); - } - - /** - * Set the current value and optionally fire the onValueChange event. - * - * @param curValue - * the current value - * @param fireEvent - * fire the onValue change event if true - */ - public synchronized void setCurrentValue(Double curValue, boolean fireEvent) { - // Confine the value to the range - if (!isMinMaxInitialized() || curValue == null) { - return; - } - this.curValue = Math.max(minValue, Math.min(maxValue, curValue)); - double remainder = (this.curValue - minValue) % stepSize; - this.curValue -= remainder; - // Go to next step if more than halfway there - if ((remainder > (stepSize / 2)) && ((this.curValue + stepSize) <= maxValue)) { - this.curValue += stepSize; - } - // Redraw the knob - drawKnob(); - // Fire the ValueChangeEvent if the value actually changed - if (fireEvent && !curValue.equals(this.curValue)) { - ValueChangeEvent.fire(this, this.curValue); - } - } - - /** - * Sets whether this widget is enabled. - * - * @param enabled - * true to enable the widget, false to disable it - */ - public void setEnabled(boolean enabled) { - this.enabled = enabled; - if (enabled) { - knobImage.setResource(images.slider()); - lineElement.setPropertyString("className", "gwt-SliderBar-line"); - } else { - knobImage.setResource(images.sliderDisabled()); - lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-disabled"); - } - redraw(); - } - - /** - * Set the label formatter. - * - * @param labelFormatter - * the label formatter - */ - public void setLabelFormatter(LabelFormatter labelFormatter) { - this.tickLabelFormatter = labelFormatter; - } - - /** - * Set the max value. - * - * @param maxValue - * the current value - */ - public void setMaxValue(Double maxValue, boolean fireEvent) { - if (!Util.equalsWithNull(maxValue, this.maxValue)) { - this.maxValue = maxValue; - onMinMaxValueChanged(fireEvent); - } - } - - /** - * Set the minimum value. - * - * @param minValue - * the current value - */ - public void setMinValue(Double minValue, boolean fireEvent) { - if (!Util.equalsWithNull(minValue, this.minValue)) { - this.minValue = minValue; - onMinMaxValueChanged(fireEvent); - } - } - - /** - * Set the minimum and maximum value - * - * @param minValue - * the current value for min - * @param maxValue - * the current valuefor max - */ - public void setMinAndMaxValue(Double minValue, Double maxValue, boolean fireEvent) { - boolean changed = false; - if (!Util.equalsWithNull(minValue, this.minValue)) { - this.minValue = minValue; - changed = true; - } - if (!Util.equalsWithNull(maxValue, this.maxValue)) { - this.maxValue = maxValue; - changed = true; - } - if (changed) { - onMinMaxValueChanged(fireEvent); - } - } - - /** - * Handle value changes of min and/or max value - */ - protected void onMinMaxValueChanged(boolean fireEvent) { - redraw(); - resetCurrentValue(fireEvent); - } - - /** - * Set the number of tick labels to show on the line. Tick labels indicate the value of the slider at that point. - * Use this method to enable tick labels. - * - * If you set the number of tick labels equal to the total range divided by the step size, you will get a properly - * aligned "jumping" effect where the knob jumps between tick labels. - * - * Note that the number of tick labels displayed will be one more than the number you specify, so specify 1 labels - * to show labels on either end of the line. In other words, numTickLabels is really the number of slots between the - * labels. - * - * setNumTickLabels(0) will disable the labels. - * - * @param numTickLabels - * the number of tick labels to show - */ - public void setNumTickLabels(int numTickLabels) { - this.numTickLabels = numTickLabels; - drawTickLabels(); - } - - /** - * Set the number of ticks to show on the line. A tick is a vertical line that represents a division of the overall - * line. Use this method to enable ticks. - * - * If you set the number of ticks equal to the total range divided by the step size, you will get a properly aligned - * "jumping" effect where the knob jumps between ticks. - * - * Note that the number of ticks displayed will be one more than the number you specify, so specify 1 tick to show - * ticks on either end of the line. In other words, numTicks is really the number of slots between the ticks. - * - * setNumTicks(0) will disable ticks. - * - * @param numTicks - * the number of ticks to show - */ - public void setNumTicks(int numTicks) { - this.numTicks = numTicks; - drawTicks(); - } - - /** - * Set the step size. - * - * @param stepSize - * the current value - */ - public void setStepSize(double stepSize, boolean fireEvent) { - this.stepSize = stepSize; - resetCurrentValue(fireEvent); - } - - public void setValue(Double value) { - setCurrentValue(value, false); - } - - public void setValue(Double value, boolean fireEvent) { - setCurrentValue(value, fireEvent); - } - - /** - * Shift to the left (smaller value). - * - * @param numSteps - * the number of steps to shift - */ - public void shiftLeft(int numSteps) { - setCurrentValue(getCurrentValue() - numSteps * stepSize); - } - - /** - * Shift to the right (greater value). - * - * @param numSteps - * the number of steps to shift - */ - public void shiftRight(int numSteps) { - setCurrentValue(getCurrentValue() + numSteps * stepSize); - } - - /** - * Format the label to display above the ticks - * - * Override this method in a subclass to customize the format. By default, this method returns the integer portion - * of the value. - * - * @param value - * the value at the label - * @return the text to put in the label - */ - protected String formatTickLabel(Double value, Double previousValue) { - if (tickLabelFormatter != null) { - return tickLabelFormatter.formatLabel(this, value, previousValue); - } else { - return (int) (10 * value) / 10.0 + ""; - } - } - - /** - * Get the percentage of the knob's position relative to the size of the line. The return value will be between 0.0 - * and 1.0. - * - * @return the current percent complete - */ - protected double getKnobPercent() { - // If we have no range - if (maxValue <= minValue) { - return 0; - } - - // Calculate the relative progress - double percent = (curValue - minValue) / (maxValue - minValue); - return Math.max(0.0, Math.min(1.0, percent)); - } - - /** - * This method is called immediately after a widget becomes attached to the browser's document. - */ - @Override - protected void onLoad() { - // Reset the position attribute of the parent element - getElement().getStyle().setPosition(Position.RELATIVE); - } - - /** - * Draw the knob where it is supposed to be relative to the line. - */ - protected void drawKnob() { - if (isAttached() && isMinMaxInitialized()) { - // Move the knob to the correct position - Element knobElement = knobImage.getElement(); - int lineWidth = lineElement.getOffsetWidth(); - int knobWidth = knobElement.getOffsetWidth(); - int knobLeftOffset = (int) (lineLeftOffset + (getKnobPercent() * lineWidth) - (knobWidth / 2)); - knobLeftOffset = Math.min(knobLeftOffset, lineLeftOffset + lineWidth - (knobWidth / 2) - 1); - knobElement.getStyle().setLeft(knobLeftOffset, Unit.PX); - } - } - - /** - * Draw the labels along the line. - */ - protected void drawTickLabels() { - if (isAttached() && isMinMaxInitialized()) { - // Draw the tick labels - int lineWidth = lineElement.getOffsetWidth(); - if (numTickLabels > 0) { - // Create the labels or make them visible - Double previousValue = null; - for (int i = 0; i <= numTickLabels; i++) { - Element label = null; - if (i < tickLabelElements.size()) { - label = tickLabelElements.get(i); - } else { // Create the new label - label = DOM.createDiv(); - label.getStyle().setPosition(Position.ABSOLUTE); - label.getStyle().setDisplay(Display.NONE); - if (enabled) { - label.setPropertyString("className", "gwt-SliderBar-ticklabel"); - } else { - label.setPropertyString("className", "gwt-SliderBar-ticklabel-disabled"); - } - DOM.appendChild(getElement(), label); - tickLabelElements.add(label); - } - - // Set the label text - double value = minValue + (getTotalRange() * i / numTickLabels); - label.getStyle().setVisibility(Visibility.HIDDEN); - label.getStyle().setProperty("display", ""); - label.setPropertyString("innerHTML", formatTickLabel(value, previousValue)); - - // Move to the left so the label width is not clipped by the shell - label.getStyle().setLeft(0, Unit.PX); - - // Position the label and make it visible - int labelWidth = label.getOffsetWidth(); - int labelLeftOffset = lineLeftOffset + (lineWidth * i / numTickLabels) - (labelWidth / 2); - labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); - labelLeftOffset = Math.max(labelLeftOffset, lineLeftOffset); - label.getStyle().setLeft(labelLeftOffset, Unit.PX); - label.getStyle().setVisibility(Visibility.VISIBLE); - - previousValue = value; - } - - // Hide unused labels - for (int i = (numTickLabels + 1); i < tickLabelElements.size(); i++) { - tickLabelElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all labels - for (Element elem : tickLabelElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Draw the tick along the line. - */ - protected void drawTicks() { - if (isAttached() && isMinMaxInitialized()) { - // Draw the ticks - int lineWidth = lineElement.getOffsetWidth(); - if (numTicks > 0) { - // Create the ticks or make them visible - for (int i = 0; i <= numTicks; i++) { - Element tick = null; - if (i < tickElements.size()) { - tick = tickElements.get(i); - } else { // Create the new tick - tick = DOM.createDiv(); - tick.getStyle().setPosition(Position.ABSOLUTE); - tick.getStyle().setDisplay(Display.NONE); - DOM.appendChild(getElement(), tick); - tickElements.add(tick); - } - if (enabled) { - tick.setPropertyString("className", "gwt-SliderBar-tick"); - } else { - tick.setPropertyString("className", "gwt-SliderBar-tick gwt-SliderBar-tick-disabled"); - } - // Position the tick and make it visible - tick.getStyle().setVisibility(Visibility.HIDDEN); - tick.getStyle().setProperty("display", ""); - int tickWidth = tick.getOffsetWidth(); - int tickLeftOffset = lineLeftOffset + (lineWidth * i / numTicks) - (tickWidth / 2); - tickLeftOffset = Math.min(tickLeftOffset, lineLeftOffset + lineWidth - tickWidth); - tick.getStyle().setLeft(tickLeftOffset, Unit.PX); - tick.getStyle().setVisibility(Visibility.VISIBLE); - } - - // Hide unused ticks - for (int i = (numTicks + 1); i < tickElements.size(); i++) { - tickElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all ticks - for (Element elem : tickElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Draw the markers. - */ - private void drawMarkers() { - if (isAttached() && isMinMaxInitialized()) { - int numMarkers = markers.size(); - // Draw the markers - int lineWidth = lineElement.getOffsetWidth(); - if (numMarkers > 0) { - // Create the markers or make them visible - Element lastMarkerElement = null; - int lastMarkerOffsetLeft = 0; - for (int i = 0; i < numMarkers; i++) { - Marker marker = markers.get(i); - Element markerElem = null; - if (i < markerElements.size()) { - markerElem = markerElements.get(i); - } else { // Create the new markes - markerElem = DOM.createDiv(); - markerElem.getStyle().setPosition(Position.ABSOLUTE); - markerElem.getStyle().setDisplay(Display.NONE); - DOM.appendChild(getElement(), markerElem); - markerElements.add(markerElem); - } - if (enabled) { - markerElem.setPropertyString("className", "gwt-SliderBar-mark"); - } else { - markerElem.setPropertyString("className", "gwt-SliderBar-mark gwt-SliderBar-mark-disabled"); - } - // Position the marker and make it visible - markerElem.getStyle().setVisibility(Visibility.HIDDEN); - markerElem.getStyle().setProperty("display", ""); - double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); - int markerWidth = markerElem.getOffsetWidth(); - int markerLeftOffset = lineLeftOffset + (int) markerLinePosition - (markerWidth / 2); - markerLeftOffset = Math.min(markerLeftOffset, lineLeftOffset + lineWidth - markerWidth); - markerElem.getStyle().setLeft(markerLeftOffset, Unit.PX); - markerElem.getStyle().setVisibility(Visibility.VISIBLE); - // change class of last marker if it is too close to the one just created - if (lastMarkerOffsetLeft > 0 && (markerLeftOffset-lastMarkerOffsetLeft) <= markerWidth) { - if (lastMarkerElement != null) { - lastMarkerElement.setPropertyString("className", "gwt-SliderBar-mark-tooClose"); - lastMarkerElement.getStyle().setLeft(markerLeftOffset-10, Unit.PX); - } - } - lastMarkerElement = markerElem; - lastMarkerOffsetLeft = markerLeftOffset; - } - - // Hide unused markers - for (int i = numMarkers; i < markerElements.size(); i++) { - markerElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all markers - for (Element elem : markerElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Draw the marker labels. - */ - private void drawMarkerLabels() { - if (isAttached() && isMinMaxInitialized()) { - int numMarkers = markers.size(); - // Draw the marker labels - int lineWidth = lineElement.getOffsetWidth(); - if (numMarkers > 0) { - Element lastMarkerLabel = null; - Marker lastMarker = null; - int lastMarkerLabelOffsetLeft = 0; - // Create the labels or make them visible - for (int i = 0; i < numMarkers; i++) { - Marker marker = markers.get(i); - Element label = null; - if (i < markerLabelElements.size()) { - label = markerLabelElements.get(i); - } else { // Create the new label - label = DOM.createDiv(); - label.getStyle().setPosition(Position.ABSOLUTE); - label.getStyle().setDisplay(Display.NONE); - if (enabled) { - label.setPropertyString("className", "gwt-SliderBar-markerlabel"); - } else { - label.setPropertyString("className", "gwt-SliderBar-markerlabel-disabled"); - } - DOM.appendChild(getElement(), label); - markerLabelElements.add(label); - } - - // Set the marker label text - label.getStyle().setVisibility(Visibility.HIDDEN); - label.getStyle().setProperty("display", ""); - label.setPropertyString("innerHTML", marker.name); - - // Move to the left so the label width is not clipped by the shell - label.getStyle().setLeft(0, Unit.PX); - - // Position the label and make it visible - double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); - int labelWidth = label.getOffsetWidth(); - int labelLeftOffset = lineLeftOffset + (int) markerLinePosition - (labelWidth / 2); - labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); - - label.getStyle().setLeft(labelLeftOffset, Unit.PX); - label.getStyle().setVisibility(Visibility.VISIBLE); - - // hide last marker label if it is too close to the one just created - if (lastMarkerLabelOffsetLeft > 0 && (labelLeftOffset-lastMarkerLabelOffsetLeft) <= 20) { - if (lastMarkerLabel != null && lastMarker != null) { - lastMarkerLabel.setPropertyString("className", "gwt-SliderBar-markerlabel-tooClose"); - lastMarkerLabel.setPropertyString("innerHTML", lastMarker.name); // no space for more - int currentMarkerOffsetLeft = labelLeftOffset; - try { - currentMarkerOffsetLeft = Integer.parseInt(markerElements.get(i).getStyle().getLeft().replace("px", "")); - } catch (NumberFormatException ex) { - // ignore, can't do anything - } - lastMarkerLabel.getStyle().setLeft(currentMarkerOffsetLeft-8, Unit.PX); - } - } - lastMarker = marker; - lastMarkerLabel = label; - lastMarkerLabelOffsetLeft = labelLeftOffset; - } - - // Hide unused labels - for (int i = (numMarkers + 1); i < markerLabelElements.size(); i++) { - markerLabelElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all labels - for (Element elem : markerLabelElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Highlight this widget. - */ - private void highlight() { - String styleName = getStylePrimaryName(); - getElement().setPropertyString("className", styleName + " " + styleName + "-focused"); - } - - /** - * Reset the progress to constrain the progress to the current range and redraw the knob as needed. - */ - private synchronized void resetCurrentValue(boolean fireEvent) { - setCurrentValue(getCurrentValue(), fireEvent); - } - - /** - * Slide the knob to a new location. - * - * @param event - * the mouse event - */ - private void slideKnob(Event event) { - // Adding scrollLeft to adjust the position, if the user had scrolled with the lower scroll bar - int x = event.getClientX() + Window.getScrollLeft(); - if (x > 0) { - int lineWidth = lineElement.getOffsetWidth(); - int lineLeft = lineElement.getAbsoluteLeft(); - double percent = (double) (x - lineLeft) / lineWidth * 1.0; - setCurrentValue(getTotalRange() * percent + minValue, true); - } - } - - /** - * Start sliding the knob. - * - * @param highlight - * true to change the style - * @param fireEvent - * true to fire the event - */ - private void startSliding(boolean highlight, boolean fireEvent) { - if (highlight) { - lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-sliding"); - knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob gwt-SliderBar-knob-sliding"); - knobImage.setResource(images.sliderSliding()); - } - } - - /** - * Stop sliding the knob. - * - * @param unhighlight - * true to change the style - * @param fireEvent - * true to fire the event - */ - private void stopSliding(boolean unhighlight, boolean fireEvent) { - if (unhighlight) { - lineElement.setPropertyString("className", "gwt-SliderBar-line"); - knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob"); - knobImage.setResource(images.slider()); - } - } - - /** - * Unhighlight this widget. - */ - private void unhighlight() { - getElement().setPropertyString("className", getStylePrimaryName()); - } - - @Override - public void onResize() { - redraw(); - } - - public void clearMarkers() { - markers.clear(); - } - - public boolean addMarker(String markerName, Double markerPosition) { - return markers.add(new Marker(markerName, markerPosition)); - } - - public boolean setMarker(String markerName, Double markerPosition) { - Marker marker = findMarkerByName(markerName); - if (marker != null) { - marker.position = markerPosition; - return true; - } - return false; - } - - public boolean removeMarker(String markerName) { - Marker marker = findMarkerByName(markerName); - if (marker != null) { - return markers.remove(marker); - } - return false; - } - - public Iterator getMarkers() { - return markers.iterator(); - } - - private Marker findMarkerByName(String markerName) { - for (Marker marker : markers) { - if (marker.name.equals(markerName)) { - return marker; - } - } - return null; - } -} +package com.sap.sse.gwt.client.controls.slider; + +/* + * Copyright 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +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.Element; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.Style.Visibility; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.logical.shared.HasValueChangeHandlers; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.resources.client.CssResource.NotStrict; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.FocusPanel; +import com.google.gwt.user.client.ui.HasValue; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.RequiresResize; +import com.sap.sse.common.Util; + +/** + * A widget that allows the user to select a value within a range of possible values using a sliding bar that responds + * to mouse events. + * + *

    Keyboard Events

    + *

    + * SliderBar listens for the following key events. Holding down a key will repeat the action until the key is released. + *

      + *
    • left arrow - shift left one step
    • + *
    • right arrow - shift right one step
    • + *
    • ctrl+left arrow - jump left 10% of the distance
    • + *
    • ctrl+right arrow - jump right 10% of the distance
    • + *
    • home - jump to min value
    • + *
    • end - jump to max value
    • + *
    • space - jump to middle value
    • + *
    + *

    + * + *

    CSS Style Rules

    + *
      + *
    • .gwt-SliderBar-shell { primary style }
    • + *
    • .gwt-SliderBar-shell-focused { primary style when focused }
    • + *
    • .gwt-SliderBar-shell gwt-SliderBar-line { the line that the knob moves along }
    • + *
    • .gwt-SliderBar-shell gwt-SliderBar-line-sliding { the line that the knob moves along when sliding }
    • + *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob { the sliding knob }
    • + *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob-sliding { the sliding knob when sliding }
    • + *
    • + * .gwt-SliderBar-shell .gwt-SliderBar-tick { the ticks along the line }
    • + *
    • .gwt-SliderBar-shell .gwt-SliderBar-ticklabel { the text labels along the line }
    • + *
    + */ +public class SliderBar extends FocusPanel implements RequiresResize, HasValue, HasValueChangeHandlers { + /** + * A formatter used to format the labels displayed in the widget. + */ + public static interface LabelFormatter { + /** + * Generate the text to display in each label based on the label's value. + * + * Override this method to change the text displayed within the SliderBar. + * + * @param slider + * the Slider bar + * @param value + * the value the label displays + * @return the text to display for the label + */ + String formatLabel(SliderBar slider, Double value, Double previousValue); + } + + /** + * A {@link ClientBundle} that provides images for {@link SliderBar}. + */ + public static interface SliderBarImages extends ClientBundle { + public static final SliderBarImages INSTANCE = GWT.create(SliderBarImages.class); + + /** + * An image used for the sliding knob. + * + * @return a prototype of this image + */ + @Source("slider_blue.png") + ImageResource slider(); + + /** + * An image used for the disabled sliding knob. + * + * @return a prototype of this image + */ + @Source("slider_blue.png") + ImageResource sliderDisabled(); + + /** + * An image used for the sliding knob while sliding. + * + * @return a prototype of this image + */ + @Source("slider_blue.png") + ImageResource sliderSliding(); + + @NotStrict + @Source("SliderBar.css") + CssResource sliderBarCss(); + } + + /** + * The timer used to continue to shift the knob as the user holds down one of the left/right arrow keys. Only IE + * auto-repeats, so we just keep catching the events. + */ + private class KeyTimer extends Timer { + /** + * A bit indicating that this is the first run. + */ + private boolean firstRun = true; + + /** + * The delay between shifts, which shortens as the user holds down the button. + */ + private int repeatDelay = 30; + + /** + * A bit indicating whether we are shifting to a higher or lower value. + */ + private boolean shiftRight = false; + + /** + * The number of steps to shift with each press. + */ + private int multiplier = 1; + + /** + * This method will be called when a timer fires. Override it to implement the timer's logic. + */ + @Override + public void run() { + // Highlight the knob on first run + if (firstRun) { + firstRun = false; + startSliding(true, false); + } + + // Slide the slider bar + if (shiftRight) { + setCurrentValue(curValue + multiplier * stepSize); + } else { + setCurrentValue(curValue - multiplier * stepSize); + } + + // Repeat this timer until cancelled by keyup event + schedule(repeatDelay); + } + + /** + * Schedules a timer to elapse in the future. + * + * @param delayMillis + * how long to wait before the timer elapses, in milliseconds + * @param shiftRight + * whether to shift up or not + * @param multiplier + * the number of steps to shift + */ + public void schedule(int delayMillis, boolean shiftRight, int multiplier) { + firstRun = true; + this.shiftRight = shiftRight; + this.multiplier = multiplier; + super.schedule(delayMillis); + } + } + + /** + * The current value. + */ + protected Double curValue; + + /** + * The knob that slides across the line. + */ + protected Image knobImage = new Image(); + + /** + * The timer used to continue to shift the knob if the user holds down a key. + */ + private KeyTimer keyTimer = new KeyTimer(); + + /** + * The elements used to display labels above the ticks. + */ + protected List tickLabelElements = new ArrayList(); + + /** + * The elements used to display the marker labels. + */ + protected List markerLabelElements = new ArrayList(); + + /** + * The formatter used to generate label text. + */ + protected LabelFormatter tickLabelFormatter; + + /** + * The line that the knob moves over. + */ + protected Element lineElement; + + /** + * The offset between the edge of the shell and the line. + */ + protected int lineLeftOffset = 0; + + /** + * The maximum slider value. + */ + protected Double maxValue; + + /** + * The minimum slider value. + */ + protected Double minValue; + + /** + * The number of labels to show. + */ + private int numTickLabels = 0; + + /** + * The number of tick marks to show. + */ + protected int numTicks = 0; + + /** + * A bit indicating whether or not we are currently sliding the slider bar due to keyboard events. + */ + private boolean slidingKeyboard = false; + + /** + * A bit indicating whether or not we are currently sliding the slider bar due to mouse events. + */ + private boolean slidingMouse = false; + + /** + * A bit indicating whether or not the slider is enabled + */ + protected boolean enabled = true; + + /** + * The images used with the sliding bar. + */ + private SliderBarImages images; + + /** + * The size of the increments between knob positions. + */ + protected double stepSize; + + /** + * The elements used to display tick marks, which are the vertical lines along the slider bar. + */ + protected List tickElements = new ArrayList(); + + /** + * The elements used to display additional markers on the slider bar. + */ + protected List markerElements = new ArrayList(); + + private List markers = new ArrayList(); + + private class Marker { + String name; + Double position; + + public Marker(String name, Double position) { + super(); + this.name = name; + this.position = position; + } + } + + /** + * Create a slider bar. + */ + public SliderBar() { + this(null, null, null); + } + + /** + * Create a slider bar. + * + * @param minValue + * the minimum value in the range + * @param maxValue + * the maximum value in the range + */ + public SliderBar(double minValue, double maxValue) { + this(minValue, maxValue, null); + } + + /** + * Create a slider bar. + * + * @param minValue + * the minimum value in the range + * @param maxValue + * the maximum value in the range + * @param tickLabelFormatter + * the label formatter + */ + public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter) { + this(minValue, maxValue, tickLabelFormatter, SliderBarImages.INSTANCE); + } + + /** + * Create a slider bar. + * + * @param minValue + * the minimum value in the range + * @param maxValue + * the maximum value in the range + * @param tickLabelFormatter + * the label formatter + * @param images + * the images to use for the slider + */ + public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter, SliderBarImages images) { + super(); + images.sliderBarCss().ensureInjected(); + this.minValue = minValue; + this.maxValue = maxValue; + this.images = images; + setLabelFormatter(tickLabelFormatter); + + // Create the outer shell + getElement().getStyle().setPosition(Position.RELATIVE); + setStyleName("gwt-SliderBar-shell"); + + // Create the line + lineElement = DOM.createDiv(); + DOM.appendChild(getElement(), lineElement); + lineElement.getStyle().setPosition(Position.ABSOLUTE); + lineElement.setPropertyString("className", "gwt-SliderBar-line"); + + // Create the knob + knobImage.setResource(images.slider()); + Element knobElement = knobImage.getElement(); + DOM.appendChild(getElement(), knobElement); + knobElement.getStyle().setPosition(Position.ABSOLUTE); + knobElement.setPropertyString("className", "gwt-SliderBar-knob"); + + sinkEvents(Event.MOUSEEVENTS | Event.KEYEVENTS | Event.FOCUSEVENTS); + + // workaround to render properly when parent Widget does not + // implement ProvidesResize since DOM doesn't provide element + // height and width until onModuleLoad() finishes. + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + onResize(); + } + }); + } + + public boolean isMinMaxInitialized() { + return minValue != null && maxValue != null; + } + + public HandlerRegistration addValueChangeHandler(ValueChangeHandler handler) { + return addHandler(handler, ValueChangeEvent.getType()); + } + + /** + * Return the current value. + * + * @return the current value + */ + public Double getCurrentValue() { + return curValue; + } + + /** + * Return the label formatter. + * + * @return the label formatter + */ + public LabelFormatter getLabelFormatter() { + return tickLabelFormatter; + } + + /** + * Return the max value. + * + * @return the max value + */ + public Double getMaxValue() { + return maxValue; + } + + /** + * Return the minimum value. + * + * @return the minimum value + */ + public Double getMinValue() { + return minValue; + } + + /** + * Return the number of labels. + * + * @return the number of labels + */ + public int getNumLabels() { + return numTickLabels; + } + + /** + * Return the number of ticks. + * + * @return the number of ticks + */ + public int getNumTicks() { + return numTicks; + } + + /** + * Return the step size. + * + * @return the step size + */ + public double getStepSize() { + return stepSize; + } + + /** + * Return the total range between the minimum and maximum values. + * + * @return the total range + */ + public double getTotalRange() { + if (minValue == null || maxValue == null || minValue > maxValue) { + return 0; + } else { + return maxValue - minValue; + } + } + + public Double getValue() { + return curValue; + } + + /** + * @return Gets whether this widget is enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Listen for events that will move the knob. + * + * @param event + * the event that occurred + */ + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (enabled) { + switch (DOM.eventGetType(event)) { + // Unhighlight and cancel keyboard events + case Event.ONBLUR: + keyTimer.cancel(); + if (slidingMouse) { + DOM.releaseCapture(getElement()); + slidingMouse = false; + slideKnob(event); + stopSliding(true, true); + } else if (slidingKeyboard) { + slidingKeyboard = false; + stopSliding(true, true); + } + unhighlight(); + break; + + // Highlight on focus + case Event.ONFOCUS: + highlight(); + break; + + // Mousewheel events + case Event.ONMOUSEWHEEL: + int velocityY = event.getMouseWheelVelocityY(); + event.preventDefault(); + if (velocityY > 0) { + shiftRight(1); + } else { + shiftLeft(1); + } + break; + + // Shift left or right on key press + case Event.ONKEYDOWN: + if (!slidingKeyboard) { + int multiplier = 1; + if (event.getCtrlKey()) { + multiplier = (int) (getTotalRange() / stepSize / 10); + } + + switch (event.getKeyCode()) { + case KeyCodes.KEY_HOME: + event.preventDefault(); + setCurrentValue(minValue); + break; + case KeyCodes.KEY_END: + event.preventDefault(); + setCurrentValue(maxValue); + break; + case KeyCodes.KEY_LEFT: + event.preventDefault(); + slidingKeyboard = true; + startSliding(false, true); + shiftLeft(multiplier); + keyTimer.schedule(400, false, multiplier); + break; + case KeyCodes.KEY_RIGHT: + event.preventDefault(); + slidingKeyboard = true; + startSliding(false, true); + shiftRight(multiplier); + keyTimer.schedule(400, true, multiplier); + break; + case 32: + event.preventDefault(); + setCurrentValue(minValue + getTotalRange() / 2); + break; + } + } + break; + // Stop shifting on key up + case Event.ONKEYUP: + keyTimer.cancel(); + if (slidingKeyboard) { + slidingKeyboard = false; + stopSliding(true, true); + } + break; + + // Mouse Events + case Event.ONMOUSEDOWN: + setFocus(true); + slidingMouse = true; + DOM.setCapture(getElement()); + startSliding(true, true); + event.preventDefault(); + slideKnob(event); + break; + case Event.ONMOUSEUP: + if (slidingMouse) { + DOM.releaseCapture(getElement()); + slidingMouse = false; + slideKnob(event); + stopSliding(true, true); + } + break; + case Event.ONMOUSEMOVE: + if (slidingMouse) { + slideKnob(event); + } + break; + } + } + } + + /** + * This method is called when the dimensions of the parent element change. Subclasses should override this method as + * needed. + * + * @param width + * the new client width of the element + * @param height + * the new client height of the element + */ + public void onResize(int width, int height) { + // Center the line in the shell + int lineWidth = lineElement.getOffsetWidth(); + lineLeftOffset = (width / 2) - (lineWidth / 2); + lineElement.getStyle().setLeft(lineLeftOffset, Unit.PX); + + // Draw the other components + drawTickLabels(); + drawTicks(); + drawMarkers(); + drawMarkerLabels(); + drawKnob(); + } + + /** + * Redraw the progress bar when something changes the layout. + */ + public void redraw() { + if (isAttached()) { + int width = getElement().getClientWidth(); + int height = getElement().getClientHeight(); + onResize(width, height); + } + } + + /** + * Set the current value and fire the onValueChange event. + * + * @param curValue + * the current value + */ + public synchronized void setCurrentValue(Double curValue) { + setCurrentValue(curValue, true); + } + + /** + * Set the current value and optionally fire the onValueChange event. + * + * @param curValue + * the current value + * @param fireEvent + * fire the onValue change event if true + */ + public synchronized void setCurrentValue(Double curValue, boolean fireEvent) { + // Confine the value to the range + if (!isMinMaxInitialized() || curValue == null) { + return; + } + this.curValue = Math.max(minValue, Math.min(maxValue, curValue)); + double remainder = (this.curValue - minValue) % stepSize; + this.curValue -= remainder; + // Go to next step if more than halfway there + if ((remainder > (stepSize / 2)) && ((this.curValue + stepSize) <= maxValue)) { + this.curValue += stepSize; + } + // Redraw the knob + drawKnob(); + // Fire the ValueChangeEvent if the value actually changed + if (fireEvent && !curValue.equals(this.curValue)) { + ValueChangeEvent.fire(this, this.curValue); + } + } + + /** + * Sets whether this widget is enabled. + * + * @param enabled + * true to enable the widget, false to disable it + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if (enabled) { + knobImage.setResource(images.slider()); + lineElement.setPropertyString("className", "gwt-SliderBar-line"); + } else { + knobImage.setResource(images.sliderDisabled()); + lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-disabled"); + } + redraw(); + } + + /** + * Set the label formatter. + * + * @param labelFormatter + * the label formatter + */ + public void setLabelFormatter(LabelFormatter labelFormatter) { + this.tickLabelFormatter = labelFormatter; + } + + /** + * Set the max value. + * + * @param maxValue + * the current value + */ + public void setMaxValue(Double maxValue, boolean fireEvent) { + if (!Util.equalsWithNull(maxValue, this.maxValue)) { + this.maxValue = maxValue; + onMinMaxValueChanged(fireEvent); + } + } + + /** + * Set the minimum value. + * + * @param minValue + * the current value + */ + public void setMinValue(Double minValue, boolean fireEvent) { + if (!Util.equalsWithNull(minValue, this.minValue)) { + this.minValue = minValue; + onMinMaxValueChanged(fireEvent); + } + } + + /** + * Set the minimum and maximum value + * + * @param minValue + * the current value for min + * @param maxValue + * the current valuefor max + */ + public void setMinAndMaxValue(Double minValue, Double maxValue, boolean fireEvent) { + boolean changed = false; + if (!Util.equalsWithNull(minValue, this.minValue)) { + this.minValue = minValue; + changed = true; + } + if (!Util.equalsWithNull(maxValue, this.maxValue)) { + this.maxValue = maxValue; + changed = true; + } + if (changed) { + onMinMaxValueChanged(fireEvent); + } + } + + /** + * Handle value changes of min and/or max value + */ + protected void onMinMaxValueChanged(boolean fireEvent) { + redraw(); + resetCurrentValue(fireEvent); + } + + /** + * Set the number of tick labels to show on the line. Tick labels indicate the value of the slider at that point. + * Use this method to enable tick labels. + * + * If you set the number of tick labels equal to the total range divided by the step size, you will get a properly + * aligned "jumping" effect where the knob jumps between tick labels. + * + * Note that the number of tick labels displayed will be one more than the number you specify, so specify 1 labels + * to show labels on either end of the line. In other words, numTickLabels is really the number of slots between the + * labels. + * + * setNumTickLabels(0) will disable the labels. + * + * @param numTickLabels + * the number of tick labels to show + */ + public void setNumTickLabels(int numTickLabels) { + this.numTickLabels = numTickLabels; + drawTickLabels(); + } + + /** + * Set the number of ticks to show on the line. A tick is a vertical line that represents a division of the overall + * line. Use this method to enable ticks. + * + * If you set the number of ticks equal to the total range divided by the step size, you will get a properly aligned + * "jumping" effect where the knob jumps between ticks. + * + * Note that the number of ticks displayed will be one more than the number you specify, so specify 1 tick to show + * ticks on either end of the line. In other words, numTicks is really the number of slots between the ticks. + * + * setNumTicks(0) will disable ticks. + * + * @param numTicks + * the number of ticks to show + */ + public void setNumTicks(int numTicks) { + this.numTicks = numTicks; + drawTicks(); + } + + /** + * Set the step size. + * + * @param stepSize + * the current value + */ + public void setStepSize(double stepSize, boolean fireEvent) { + this.stepSize = stepSize; + resetCurrentValue(fireEvent); + } + + public void setValue(Double value) { + setCurrentValue(value, false); + } + + public void setValue(Double value, boolean fireEvent) { + setCurrentValue(value, fireEvent); + } + + /** + * Shift to the left (smaller value). + * + * @param numSteps + * the number of steps to shift + */ + public void shiftLeft(int numSteps) { + setCurrentValue(getCurrentValue() - numSteps * stepSize); + } + + /** + * Shift to the right (greater value). + * + * @param numSteps + * the number of steps to shift + */ + public void shiftRight(int numSteps) { + setCurrentValue(getCurrentValue() + numSteps * stepSize); + } + + /** + * Format the label to display above the ticks + * + * Override this method in a subclass to customize the format. By default, this method returns the integer portion + * of the value. + * + * @param value + * the value at the label + * @return the text to put in the label + */ + protected String formatTickLabel(Double value, Double previousValue) { + if (tickLabelFormatter != null) { + return tickLabelFormatter.formatLabel(this, value, previousValue); + } else { + return (int) (10 * value) / 10.0 + ""; + } + } + + /** + * Get the percentage of the knob's position relative to the size of the line. The return value will be between 0.0 + * and 1.0. + * + * @return the current percent complete + */ + protected double getKnobPercent() { + // If we have no range + if (maxValue <= minValue) { + return 0; + } + + // Calculate the relative progress + double percent = (curValue - minValue) / (maxValue - minValue); + return Math.max(0.0, Math.min(1.0, percent)); + } + + /** + * This method is called immediately after a widget becomes attached to the browser's document. + */ + @Override + protected void onLoad() { + // Reset the position attribute of the parent element + getElement().getStyle().setPosition(Position.RELATIVE); + } + + /** + * Draw the knob where it is supposed to be relative to the line. + */ + protected void drawKnob() { + if (isAttached() && isMinMaxInitialized()) { + // Move the knob to the correct position + Element knobElement = knobImage.getElement(); + int lineWidth = lineElement.getOffsetWidth(); + int knobWidth = knobElement.getOffsetWidth(); + int knobLeftOffset = (int) (lineLeftOffset + (getKnobPercent() * lineWidth) - (knobWidth / 2)); + knobLeftOffset = Math.min(knobLeftOffset, lineLeftOffset + lineWidth - (knobWidth / 2) - 1); + knobElement.getStyle().setLeft(knobLeftOffset, Unit.PX); + } + } + + /** + * Draw the labels along the line. + */ + protected void drawTickLabels() { + if (isAttached() && isMinMaxInitialized()) { + // Draw the tick labels + int lineWidth = lineElement.getOffsetWidth(); + if (numTickLabels > 0) { + // Create the labels or make them visible + Double previousValue = null; + for (int i = 0; i <= numTickLabels; i++) { + Element label = null; + if (i < tickLabelElements.size()) { + label = tickLabelElements.get(i); + } else { // Create the new label + label = DOM.createDiv(); + label.getStyle().setPosition(Position.ABSOLUTE); + label.getStyle().setDisplay(Display.NONE); + if (enabled) { + label.setPropertyString("className", "gwt-SliderBar-ticklabel"); + } else { + label.setPropertyString("className", "gwt-SliderBar-ticklabel-disabled"); + } + DOM.appendChild(getElement(), label); + tickLabelElements.add(label); + } + + // Set the label text + double value = minValue + (getTotalRange() * i / numTickLabels); + label.getStyle().setVisibility(Visibility.HIDDEN); + label.getStyle().setProperty("display", ""); + label.setPropertyString("innerHTML", formatTickLabel(value, previousValue)); + + // Move to the left so the label width is not clipped by the shell + label.getStyle().setLeft(0, Unit.PX); + + // Position the label and make it visible + int labelWidth = label.getOffsetWidth(); + int labelLeftOffset = lineLeftOffset + (lineWidth * i / numTickLabels) - (labelWidth / 2); + labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); + labelLeftOffset = Math.max(labelLeftOffset, lineLeftOffset); + label.getStyle().setLeft(labelLeftOffset, Unit.PX); + label.getStyle().setVisibility(Visibility.VISIBLE); + + previousValue = value; + } + + // Hide unused labels + for (int i = (numTickLabels + 1); i < tickLabelElements.size(); i++) { + tickLabelElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all labels + for (Element elem : tickLabelElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Draw the tick along the line. + */ + protected void drawTicks() { + if (isAttached() && isMinMaxInitialized()) { + // Draw the ticks + int lineWidth = lineElement.getOffsetWidth(); + if (numTicks > 0) { + // Create the ticks or make them visible + for (int i = 0; i <= numTicks; i++) { + Element tick = null; + if (i < tickElements.size()) { + tick = tickElements.get(i); + } else { // Create the new tick + tick = DOM.createDiv(); + tick.getStyle().setPosition(Position.ABSOLUTE); + tick.getStyle().setDisplay(Display.NONE); + DOM.appendChild(getElement(), tick); + tickElements.add(tick); + } + if (enabled) { + tick.setPropertyString("className", "gwt-SliderBar-tick"); + } else { + tick.setPropertyString("className", "gwt-SliderBar-tick gwt-SliderBar-tick-disabled"); + } + // Position the tick and make it visible + tick.getStyle().setVisibility(Visibility.HIDDEN); + tick.getStyle().setProperty("display", ""); + int tickWidth = tick.getOffsetWidth(); + int tickLeftOffset = lineLeftOffset + (lineWidth * i / numTicks) - (tickWidth / 2); + tickLeftOffset = Math.min(tickLeftOffset, lineLeftOffset + lineWidth - tickWidth); + tick.getStyle().setLeft(tickLeftOffset, Unit.PX); + tick.getStyle().setVisibility(Visibility.VISIBLE); + } + + // Hide unused ticks + for (int i = (numTicks + 1); i < tickElements.size(); i++) { + tickElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all ticks + for (Element elem : tickElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Draw the markers. + */ + private void drawMarkers() { + if (isAttached() && isMinMaxInitialized()) { + int numMarkers = markers.size(); + // Draw the markers + int lineWidth = lineElement.getOffsetWidth(); + if (numMarkers > 0) { + // Create the markers or make them visible + Element lastMarkerElement = null; + int lastMarkerOffsetLeft = 0; + for (int i = 0; i < numMarkers; i++) { + Marker marker = markers.get(i); + Element markerElem = null; + if (i < markerElements.size()) { + markerElem = markerElements.get(i); + } else { // Create the new markes + markerElem = DOM.createDiv(); + markerElem.getStyle().setPosition(Position.ABSOLUTE); + markerElem.getStyle().setDisplay(Display.NONE); + DOM.appendChild(getElement(), markerElem); + markerElements.add(markerElem); + } + if (enabled) { + markerElem.setPropertyString("className", "gwt-SliderBar-mark"); + } else { + markerElem.setPropertyString("className", "gwt-SliderBar-mark gwt-SliderBar-mark-disabled"); + } + // Position the marker and make it visible + markerElem.getStyle().setVisibility(Visibility.HIDDEN); + markerElem.getStyle().setProperty("display", ""); + double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); + int markerWidth = markerElem.getOffsetWidth(); + int markerLeftOffset = lineLeftOffset + (int) markerLinePosition - (markerWidth / 2); + markerLeftOffset = Math.min(markerLeftOffset, lineLeftOffset + lineWidth - markerWidth); + markerElem.getStyle().setLeft(markerLeftOffset, Unit.PX); + markerElem.getStyle().setVisibility(Visibility.VISIBLE); + // change class of last marker if it is too close to the one just created + if (lastMarkerOffsetLeft > 0 && (markerLeftOffset-lastMarkerOffsetLeft) <= markerWidth) { + if (lastMarkerElement != null) { + lastMarkerElement.setPropertyString("className", "gwt-SliderBar-mark-tooClose"); + lastMarkerElement.getStyle().setLeft(markerLeftOffset-10, Unit.PX); + } + } + lastMarkerElement = markerElem; + lastMarkerOffsetLeft = markerLeftOffset; + } + + // Hide unused markers + for (int i = numMarkers; i < markerElements.size(); i++) { + markerElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all markers + for (Element elem : markerElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Draw the marker labels. + */ + private void drawMarkerLabels() { + if (isAttached() && isMinMaxInitialized()) { + int numMarkers = markers.size(); + // Draw the marker labels + int lineWidth = lineElement.getOffsetWidth(); + if (numMarkers > 0) { + Element lastMarkerLabel = null; + Marker lastMarker = null; + int lastMarkerLabelOffsetLeft = 0; + // Create the labels or make them visible + for (int i = 0; i < numMarkers; i++) { + Marker marker = markers.get(i); + Element label = null; + if (i < markerLabelElements.size()) { + label = markerLabelElements.get(i); + } else { // Create the new label + label = DOM.createDiv(); + label.getStyle().setPosition(Position.ABSOLUTE); + label.getStyle().setDisplay(Display.NONE); + DOM.appendChild(getElement(), label); + markerLabelElements.add(label); + } + if (enabled) { + label.setPropertyString("className", "gwt-SliderBar-markerlabel"); + } else { + label.setPropertyString("className", "gwt-SliderBar-markerlabel-disabled"); + } + + // Set the marker label text + label.getStyle().setVisibility(Visibility.HIDDEN); + label.getStyle().setProperty("display", ""); + label.setPropertyString("innerHTML", marker.name); + + // Move to the left so the label width is not clipped by the shell + label.getStyle().setLeft(0, Unit.PX); + + // Position the label and make it visible + double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); + int labelWidth = label.getOffsetWidth(); + int labelLeftOffset = lineLeftOffset + (int) markerLinePosition - (labelWidth / 2); + labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); + + label.getStyle().setLeft(labelLeftOffset, Unit.PX); + label.getStyle().setVisibility(Visibility.VISIBLE); + + // hide last marker label if it is too close to the one just created + if (lastMarkerLabelOffsetLeft > 0 && (labelLeftOffset-lastMarkerLabelOffsetLeft) <= 20) { + if (lastMarkerLabel != null && lastMarker != null) { + lastMarkerLabel.setPropertyString("className", "gwt-SliderBar-markerlabel-tooClose"); + lastMarkerLabel.setPropertyString("innerHTML", lastMarker.name); // no space for more + int currentMarkerOffsetLeft = labelLeftOffset; + try { + currentMarkerOffsetLeft = Integer.parseInt(markerElements.get(i).getStyle().getLeft().replace("px", "")); + } catch (NumberFormatException ex) { + // ignore, can't do anything + } + lastMarkerLabel.getStyle().setLeft(currentMarkerOffsetLeft-8, Unit.PX); + } + } + lastMarker = marker; + lastMarkerLabel = label; + lastMarkerLabelOffsetLeft = labelLeftOffset; + } + + // Hide unused labels + for (int i = (numMarkers + 1); i < markerLabelElements.size(); i++) { + markerLabelElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all labels + for (Element elem : markerLabelElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Highlight this widget. + */ + private void highlight() { + String styleName = getStylePrimaryName(); + getElement().setPropertyString("className", styleName + " " + styleName + "-focused"); + } + + /** + * Reset the progress to constrain the progress to the current range and redraw the knob as needed. + */ + private synchronized void resetCurrentValue(boolean fireEvent) { + setCurrentValue(getCurrentValue(), fireEvent); + } + + /** + * Slide the knob to a new location. + * + * @param event + * the mouse event + */ + private void slideKnob(Event event) { + // Adding scrollLeft to adjust the position, if the user had scrolled with the lower scroll bar + int x = event.getClientX() + Window.getScrollLeft(); + if (x > 0) { + int lineWidth = lineElement.getOffsetWidth(); + int lineLeft = lineElement.getAbsoluteLeft(); + double percent = (double) (x - lineLeft) / lineWidth * 1.0; + setCurrentValue(getTotalRange() * percent + minValue, true); + } + } + + /** + * Start sliding the knob. + * + * @param highlight + * true to change the style + * @param fireEvent + * true to fire the event + */ + private void startSliding(boolean highlight, boolean fireEvent) { + if (highlight) { + lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-sliding"); + knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob gwt-SliderBar-knob-sliding"); + knobImage.setResource(images.sliderSliding()); + } + } + + /** + * Stop sliding the knob. + * + * @param unhighlight + * true to change the style + * @param fireEvent + * true to fire the event + */ + private void stopSliding(boolean unhighlight, boolean fireEvent) { + if (unhighlight) { + lineElement.setPropertyString("className", "gwt-SliderBar-line"); + knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob"); + knobImage.setResource(images.slider()); + } + } + + /** + * Unhighlight this widget. + */ + private void unhighlight() { + getElement().setPropertyString("className", getStylePrimaryName()); + } + + @Override + public void onResize() { + redraw(); + } + + public void clearMarkers() { + markers.clear(); + } + + public boolean addMarker(String markerName, Double markerPosition) { + return markers.add(new Marker(markerName, markerPosition)); + } + + public boolean setMarker(String markerName, Double markerPosition) { + Marker marker = findMarkerByName(markerName); + if (marker != null) { + marker.position = markerPosition; + return true; + } + return false; + } + + public boolean removeMarker(String markerName) { + Marker marker = findMarkerByName(markerName); + if (marker != null) { + return markers.remove(marker); + } + return false; + } + + public Iterator getMarkers() { + return markers.iterator(); + } + + private Marker findMarkerByName(String markerName) { + for (Marker marker : markers) { + if (marker.name.equals(markerName)) { + return marker; + } + } + return null; + } +} From 372e33526882ed93666713c757c8c052741c18d2 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 30 Nov 2015 12:06:01 +0100 Subject: [PATCH 063/306] newSelectionModel: made CompetitorsPanel refreshable --- .../gwt/ui/adminconsole/CompetitorPanel.java | 17 +++++++++-------- .../ui/adminconsole/CompetitorTableWrapper.java | 13 ++++++++++++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java index 245273a3680..52520fa7e52 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java @@ -23,6 +23,7 @@ import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.common.Util; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; /** @@ -36,7 +37,7 @@ public class CompetitorPanel extends SimplePanel { private final StringMessages stringMessages; private final ErrorReporter errorReporter; private final CompetitorTableWrapper> competitorTable; - private final MultiSelectionModel competitorSelectionModel; + private final RefreshableMultiSelectionModel refreshableCompetitorSelectionModel; private final String leaderboardName; public CompetitorPanel(final SailingServiceAsync sailingService, final StringMessages stringMessages, @@ -52,7 +53,7 @@ public class CompetitorPanel extends SimplePanel { this.errorReporter = errorReporter; this.leaderboardName = leaderboardName; this.competitorTable = new CompetitorTableWrapper<>(sailingService, stringMessages, errorReporter, /* multiSelection */ true, /* enablePager */ false); - this.competitorSelectionModel = competitorTable.getSelectionModel(); + this.refreshableCompetitorSelectionModel = (RefreshableMultiSelectionModel) competitorTable.getSelectionModel(); VerticalPanel mainPanel = new VerticalPanel(); this.setWidget(mainPanel); mainPanel.setWidth("100%"); @@ -73,7 +74,7 @@ public class CompetitorPanel extends SimplePanel { allowReloadButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { - allowUpdate(competitorSelectionModel.getSelectedSet()); + allowUpdate(refreshableCompetitorSelectionModel.getSelectedSet()); } }); buttonPanel.add(allowReloadButton); @@ -92,7 +93,7 @@ public class CompetitorPanel extends SimplePanel { @Override public void onClick(ClickEvent event) { for (CompetitorDTO c : competitorTable.getDataProvider().getList()) { - competitorSelectionModel.setSelected(c, true); + refreshableCompetitorSelectionModel.setSelected(c, true); } } }); @@ -104,7 +105,7 @@ public class CompetitorPanel extends SimplePanel { inviteCompetitorsButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { - Set competitors = competitorSelectionModel.getSelectedSet(); + Set competitors = refreshableCompetitorSelectionModel.getSelectedSet(); CompetitorInvitationHelper helper = new CompetitorInvitationHelper(sailingService, stringMessages, errorReporter); helper.inviteCompetitors(competitors, leaderboardName); @@ -132,13 +133,13 @@ public class CompetitorPanel extends SimplePanel { competitorTable.getTable().addColumn(competitorActionColumn, stringMessages.actions()); mainPanel.add(competitorTable); - competitorSelectionModel.addSelectionChangeHandler(new Handler() { + refreshableCompetitorSelectionModel.addSelectionChangeHandler(new Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { - allowReloadButton.setEnabled(!competitorSelectionModel.getSelectedSet().isEmpty()); + allowReloadButton.setEnabled(!refreshableCompetitorSelectionModel.getSelectedSet().isEmpty()); } }); - allowReloadButton.setEnabled(!competitorSelectionModel.getSelectedSet().isEmpty()); + allowReloadButton.setEnabled(!refreshableCompetitorSelectionModel.getSelectedSet().isEmpty()); if (leaderboardName != null) { refreshCompetitorList(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java index b27f1894845..a9e4be72683 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java @@ -24,6 +24,8 @@ import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.common.Color; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; public class CompetitorTableWrapper> extends TableWrapper { @@ -31,7 +33,14 @@ public class CompetitorTableWrapper> ext public CompetitorTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages,ErrorReporter errorReporter, boolean multiSelection, boolean enablePager) { - super(sailingService, stringMessages, errorReporter, multiSelection, enablePager, null /*EntityIdentityComparator for RefreshableSelectionModel*/); + super(sailingService, stringMessages, errorReporter, multiSelection, enablePager, + new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(CompetitorDTO dto1, CompetitorDTO dto2) { + return dto1.getIdAsString().equals(dto2.getIdAsString()); + } + }); ListHandler competitorColumnListHandler = getColumnSortHandler(); // competitors table @@ -244,6 +253,7 @@ public class CompetitorTableWrapper> ext @Override public void onSuccess(Iterable result) { refreshCompetitorList(result); + ((RefreshableSelectionModel) getSelectionModel()).refreshSelectionModel(result); if (callback != null) callback.onSuccess(result); } }); @@ -259,6 +269,7 @@ public class CompetitorTableWrapper> ext public void onSuccess(Iterable result) { getFilteredCompetitors(result); refreshCompetitorList(result); + ((RefreshableSelectionModel) getSelectionModel()).refreshSelectionModel(result); if (callback != null) callback.onSuccess(result); } }); From da35c864fa2497966f897238988b1722d8861c6f Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 30 Nov 2015 13:41:31 +0100 Subject: [PATCH 064/306] newSelectionModel: made RaceCourseManagementPanel refreshable --- .../adminconsole/ControlPointTableWrapper.java | 16 +++++++++++++++- .../gwt/ui/adminconsole/MarkTableWrapper.java | 2 ++ .../ui/adminconsole/WaypointTableWrapper.java | 7 +++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java index d08c9492800..41fcd120df5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java @@ -8,11 +8,20 @@ import com.sap.sailing.gwt.ui.shared.ControlPointDTO; import com.sap.sailing.gwt.ui.shared.GateDTO; import com.sap.sailing.gwt.ui.shared.MarkDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; public class ControlPointTableWrapper> extends TableWrapper { public ControlPointTableWrapper(boolean multiSelection, SailingServiceAsync sailingService, final StringMessages stringMessages, ErrorReporter errorReporter) { - super(sailingService, stringMessages, errorReporter, multiSelection, /* enablePager */ true, null /*EntityIdentityComparator for RefreshableSelectionModel*/); + super(sailingService, stringMessages, errorReporter, multiSelection, /* enablePager */ true, + new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(ControlPointDTO dto1, ControlPointDTO dto2) { + return dto1.getIdAsString().equals(dto2.getIdAsString()); + } + }); TextColumn nameColumn = new TextColumn() { @Override public String getValue(ControlPointDTO d) { @@ -35,4 +44,9 @@ public class ControlPointTableWrapper> }; table.addColumn(typeColumn, stringMessages.type()); } + @Override + public void refresh(Iterable controllPoints) { + super.refresh(controllPoints); + ((RefreshableSelectionModel) getSelectionModel()).refreshSelectionModel(controllPoints); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java index 6dff4737162..2a3f50c9174 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java @@ -15,6 +15,7 @@ import com.sap.sse.common.Color; import com.sap.sse.common.impl.AbstractColor; import com.sap.sse.common.impl.RGBColor; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; public class MarkTableWrapper> extends TableWrapper { public MarkTableWrapper(boolean multiSelection, SailingServiceAsync sailingService, StringMessages stringMessages, @@ -99,5 +100,6 @@ public class MarkTableWrapper> extends TableWr return o1.getName().compareTo(o2.getName()); } }); + ((RefreshableSelectionModel) getSelectionModel()).refreshSelectionModel(marks); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java index 0eb918c74e0..dc3a9588c0c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java @@ -9,6 +9,7 @@ import com.sap.sailing.gwt.ui.shared.GateDTO; import com.sap.sailing.gwt.ui.shared.MarkDTO; import com.sap.sailing.gwt.ui.shared.WaypointDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; public class WaypointTableWrapper> extends TableWrapper { public WaypointTableWrapper(boolean multiSelection, SailingServiceAsync sailingService, final StringMessages stringMessages, @@ -46,4 +47,10 @@ public class WaypointTableWrapper> extends }; table.addColumn(passingInstructionsColumn, stringMessages.passingInstructions()); } + + @Override + public void refresh(Iterable wayPoints) { + super.refresh(wayPoints); + ((RefreshableSelectionModel) getSelectionModel()).refreshSelectionModel(wayPoints); + } } From 9a975bb3a9dc8e62609b6117ba0cf5c85dd7cd1c Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 30 Nov 2015 15:57:32 +0100 Subject: [PATCH 065/306] newSelectionModel: made DeviceConfigurationPanel refreshable TODO define EntityIdentityComparator --- .../DeviceConfigurationListComposite.java | 27 ++++---- .../DeviceConfigurationPanel.java | 62 +++++++++++-------- .../DeviceConfigurationUserListComposite.java | 6 +- .../DeviceConfigurationUserPanel.java | 6 +- .../gwt/ui/adminconsole/MediaPanel.java | 10 +-- .../gwt/ui/client/SelectionProviderImpl.java | 7 ++- 6 files changed, 64 insertions(+), 54 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java index 5bab4bb5ab0..f1c10ff2ef0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java @@ -1,6 +1,5 @@ package com.sap.sailing.gwt.ui.adminconsole; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -15,20 +14,17 @@ import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; 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.SelectionModel; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.SelectionProvider; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; public class DeviceConfigurationListComposite extends Composite { protected static AdminConsoleTableResources tableResource = GWT.create(AdminConsoleTableResources.class); - private final SelectionModel configurationSelectionModel; + private final RefreshableSelectionModel refreshableConfigurationSelectionModel; private final CellTable configurationTable; protected ListDataProvider configurationsDataProvider; @@ -42,7 +38,8 @@ public class DeviceConfigurationListComposite extends Composite { protected final StringMessages stringMessages; public DeviceConfigurationListComposite(final SailingServiceAsync sailingService, - final SelectionProvider selectionProvider, + /*final SelectionProvider selectionProvider,*/ + final RefreshableSelectionModel refreshableSelectionModel, final ErrorReporter errorReporter, final StringMessages stringMessages) { this.sailingService = sailingService; this.errorReporter = errorReporter; @@ -62,14 +59,14 @@ public class DeviceConfigurationListComposite extends Composite { configurationTable = createConfigurationTable(); configurationTable.setVisible(true); - configurationSelectionModel = new MultiSelectionModel(); - configurationSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + refreshableConfigurationSelectionModel = refreshableSelectionModel; + /*refreshableConfigurationSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { selectionProvider.setSelection(getSelectedConfigurations()); } - }); - configurationTable.setSelectionModel(configurationSelectionModel); + });*/ + configurationTable.setSelectionModel(refreshableConfigurationSelectionModel); panel.add(configurationTable); initWidget(mainPanel); @@ -89,6 +86,7 @@ public class DeviceConfigurationListComposite extends Composite { ColumnSortEvent.fire(configurationTable, configurationTable.getColumnSortList()); noConfigurationsLabel.setVisible(false); + refreshableConfigurationSelectionModel.refreshSelectionModel(result); } @Override @@ -97,6 +95,7 @@ public class DeviceConfigurationListComposite extends Composite { noConfigurationsLabel.setVisible(true); configurationTable.setVisible(false); errorReporter.reportError("Error retrieving configuration data from server: " + caught.getMessage()); + refreshableConfigurationSelectionModel.clear(); } }); } @@ -145,16 +144,16 @@ public class DeviceConfigurationListComposite extends Composite { table.addColumn(identifierNameColumn, stringMessages.device()); return table; } - +/* private List getSelectedConfigurations() { List result = new ArrayList(); if (configurationsDataProvider != null) { for (DeviceConfigurationMatcherDTO regatta : configurationsDataProvider.getList()) { - if (configurationSelectionModel.isSelected(regatta)) { + if (refreshableConfigurationSelectionModel.isSelected(regatta)) { result.add(regatta); } } } return result; - } + }*/ } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java index 70c24a77c22..95965c7b7ee 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java @@ -1,7 +1,7 @@ package com.sap.sailing.gwt.ui.adminconsole; -import java.util.Arrays; import java.util.List; +import java.util.Set; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.dom.client.ClickEvent; @@ -13,20 +13,21 @@ import com.google.gwt.user.client.ui.HasVerticalAlignment; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; +import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; import com.sap.sailing.gwt.ui.adminconsole.DeviceConfigurationDetailComposite.DeviceConfigurationCloneListener; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.SelectionChangeListener; -import com.sap.sailing.gwt.ui.client.SelectionProvider; -import com.sap.sailing.gwt.ui.client.SelectionProviderImpl; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; -public class DeviceConfigurationPanel extends SimplePanel implements SelectionChangeListener, DeviceConfigurationCloneListener { +public class DeviceConfigurationPanel extends SimplePanel implements /*SelectionChangeListener,*/ DeviceConfigurationCloneListener { public static String renderIdentifiers(List clientIdentifiers, StringMessages stringMessages) { if (clientIdentifiers.size() == 1) { @@ -48,15 +49,35 @@ public class DeviceConfigurationPanel extends SimplePanel implements SelectionCh private DeviceConfigurationListComposite listComposite; private DeviceConfigurationDetailComposite detailComposite; - private final SelectionProvider selectionProvider; + //private final SelectionProvider selectionProvider; + private final RefreshableMultiSelectionModel refreshableMultiSelectionModel; public DeviceConfigurationPanel(SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter reporter) { this.sailingService = sailingService; this.stringMessages = stringMessages; this.errorReporter = reporter; - this.selectionProvider = new SelectionProviderImpl(true); - this.selectionProvider.addSelectionChangeListener(this); + this.refreshableMultiSelectionModel = new RefreshableMultiSelectionModel<>(); + //TODO Create an appropriate EntityIdentityComparator or define an .eqauls() method for DeviceConfigurationMatcherDTO + refreshableMultiSelectionModel.addSelectionChangeHandler(new Handler() { + + @Override + public void onSelectionChange(SelectionChangeEvent event) { + Set selectedConfigurations = refreshableMultiSelectionModel + .getSelectedSet(); + if (selectedConfigurations.size() == 1 && selectedConfigurations.iterator().hasNext()) { + detailComposite.setConfiguration(selectedConfigurations.iterator().next()); + detailComposite.setVisible(true); + } else { + detailComposite.setConfiguration(null); + detailComposite.setVisible(false); + } + removeConfigurationButton.setEnabled(!selectedConfigurations.isEmpty()); + + } + }); + //this.selectionProvider = new SelectionProviderImpl(true); + //this.selectionProvider.addSelectionChangeListener(this); setupUi(); } @@ -96,8 +117,6 @@ public class DeviceConfigurationPanel extends SimplePanel implements SelectionCh @Override public void onClick(ClickEvent event) { - detailComposite.setConfiguration(null); - detailComposite.setVisible(false); listComposite.refreshTable(); } }); @@ -110,7 +129,7 @@ public class DeviceConfigurationPanel extends SimplePanel implements SelectionCh Grid grid = new Grid(1 ,2); mainPanel.add(grid); - listComposite = createListComposite(sailingService, selectionProvider, errorReporter, stringMessages); + listComposite = createListComposite(sailingService, refreshableMultiSelectionModel, errorReporter, stringMessages); grid.setWidget(0, 0, listComposite); grid.getRowFormatter().setVerticalAlign(0, HasVerticalAlignment.ALIGN_TOP); grid.getColumnFormatter().getElement(1).getStyle().setPaddingTop(2.0, Unit.EM); @@ -119,18 +138,6 @@ public class DeviceConfigurationPanel extends SimplePanel implements SelectionCh detailComposite.setVisible(false); grid.setWidget(0, 1, detailComposite); } - - @Override - public void onSelectionChange(List selectedConfigurations) { - if (selectedConfigurations.size() == 1 && selectedConfigurations.iterator().hasNext()) { - detailComposite.setConfiguration(selectedConfigurations.iterator().next()); - detailComposite.setVisible(true); - } else { - detailComposite.setConfiguration(null); - detailComposite.setVisible(false); - } - removeConfigurationButton.setEnabled(!selectedConfigurations.isEmpty()); - } private void createConfiguration() { createConfiguration(new DeviceConfigurationDTO()); @@ -161,7 +168,8 @@ public class DeviceConfigurationPanel extends SimplePanel implements SelectionCh @Override public void onSuccess(DeviceConfigurationMatcherDTO newMatcher) { listComposite.refreshTable(); - onSelectionChange(Arrays.asList(newMatcher)); + refreshableMultiSelectionModel.clear(); + refreshableMultiSelectionModel.setSelected(newMatcher, true); } @Override @@ -182,9 +190,9 @@ public class DeviceConfigurationPanel extends SimplePanel implements SelectionCh return new DeviceConfigurationCreateMatcherDialog(stringMessages, validator, callback); } - protected DeviceConfigurationListComposite createListComposite(SailingServiceAsync sailingService, SelectionProvider selectionProvider, + protected DeviceConfigurationListComposite createListComposite(SailingServiceAsync sailingService, RefreshableSelectionModel selectionModel, ErrorReporter errorReporter, StringMessages stringMessages) { - return new DeviceConfigurationListComposite(sailingService, selectionProvider, + return new DeviceConfigurationListComposite(sailingService, selectionModel, errorReporter, stringMessages); } @@ -196,7 +204,7 @@ public class DeviceConfigurationPanel extends SimplePanel implements SelectionCh private void removeConfiguration() { detailComposite.setConfiguration(null); detailComposite.setVisible(false); - for (DeviceConfigurationMatcherDTO identifier : selectionProvider.getSelectedItems()) { + for (DeviceConfigurationMatcherDTO identifier : refreshableMultiSelectionModel.getSelectedSet()) { sailingService.removeDeviceConfiguration(identifier.type, identifier.clients, new AsyncCallback() { @Override public void onSuccess(Boolean result) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserListComposite.java index 939ef660f8b..afbe5e87279 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserListComposite.java @@ -6,17 +6,17 @@ import com.google.gwt.user.cellview.client.CellTable; import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; import com.google.gwt.user.cellview.client.TextColumn; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.SelectionProvider; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; public class DeviceConfigurationUserListComposite extends DeviceConfigurationListComposite { public DeviceConfigurationUserListComposite(SailingServiceAsync sailingService, - SelectionProvider selectionProvider, ErrorReporter errorReporter, + RefreshableSelectionModel refreshableSelectionModel, ErrorReporter errorReporter, StringMessages stringMessages) { - super(sailingService, selectionProvider, errorReporter, stringMessages); + super(sailingService, refreshableSelectionModel, errorReporter, stringMessages); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserPanel.java index b79cbad36dc..ebf1f53ca3d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserPanel.java @@ -2,10 +2,10 @@ package com.sap.sailing.gwt.ui.adminconsole; import com.sap.sailing.gwt.ui.adminconsole.DeviceConfigurationDetailComposite.DeviceConfigurationCloneListener; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; -import com.sap.sailing.gwt.ui.client.SelectionProvider; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; @@ -18,9 +18,9 @@ public class DeviceConfigurationUserPanel extends DeviceConfigurationPanel { } @Override - protected DeviceConfigurationListComposite createListComposite(SailingServiceAsync sailingService, SelectionProvider selectionProvider, + protected DeviceConfigurationListComposite createListComposite(SailingServiceAsync sailingService, RefreshableSelectionModel refreshableSelectionModel, ErrorReporter errorReporter, StringMessages stringMessages) { - return new DeviceConfigurationUserListComposite(sailingService, selectionProvider, errorReporter, stringMessages); + return new DeviceConfigurationUserListComposite(sailingService, refreshableSelectionModel, errorReporter, stringMessages); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java index 3490ec93491..5704ba698d4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java @@ -37,7 +37,6 @@ import com.google.gwt.user.client.ui.Label; import com.google.gwt.view.client.DefaultSelectionEventManager; import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.SelectionModel; -import com.google.gwt.view.client.SingleSelectionModel; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.media.MediaTrack; import com.sap.sailing.domain.common.media.MediaUtil; @@ -53,6 +52,7 @@ import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.MarkedAsyncCallback; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -76,6 +76,7 @@ public class MediaPanel extends FlowPanel { private CellTable mediaTracksTable; private ListDataProvider mediaTrackListDataProvider = new ListDataProvider(); private Date latestDate; + private RefreshableSingleSelectionModel refreshableSelectionModel; public MediaPanel(Set regattasDisplayers, SailingServiceAsync sailingService, RegattaRefresher regattaRefresher, MediaServiceAsync mediaService, ErrorReporter errorReporter, @@ -143,6 +144,7 @@ public class MediaPanel extends FlowPanel { allMediaTracks.addAll(mediaTrackListDataProvider.getList()); filterableMediaTracks.updateAll(allMediaTracks); mediaTrackListDataProvider.refresh(); + refreshableSelectionModel.refreshSelectionModel(allMediaTracks); } }); @@ -164,12 +166,12 @@ public class MediaPanel extends FlowPanel { mediaTracksTable.addColumnSortHandler(sortHandler); // Add a selection model so we can select cells. - final SelectionModel selectionModel = new SingleSelectionModel(); - mediaTracksTable.setSelectionModel(selectionModel, + refreshableSelectionModel = new RefreshableSingleSelectionModel<>(); + mediaTracksTable.setSelectionModel(refreshableSelectionModel, DefaultSelectionEventManager. createDefaultManager()); // Initialize the columns. - initTableColumns(selectionModel, sortHandler); + initTableColumns(refreshableSelectionModel, sortHandler); mediaTrackListDataProvider.addDataDisplay(mediaTracksTable); add(mediaTracksTable); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProviderImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProviderImpl.java index 458379310b7..ff60a1b9821 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProviderImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProviderImpl.java @@ -1,4 +1,4 @@ -package com.sap.sailing.gwt.ui.client; +/*package com.sap.sailing.gwt.ui.client; import java.util.ArrayList; import java.util.Arrays; @@ -63,11 +63,11 @@ public class SelectionProviderImpl implements SelectionProvider { } } - /** + *//** * Sets the universe of all items from which this selection model may select. An equal list will subsequently be * returned by {@link #getAllItems()}. Items from the {@link #getSelectedItems() selection} not in * newAllItems are removed from the selection. If this happens, the selection listeners are notified. - */ + *//* @Override public void setAllItems(List newAllItems) { allItems.clear(); @@ -95,3 +95,4 @@ public class SelectionProviderImpl implements SelectionProvider { return hasMultiSelection; } } +*/ \ No newline at end of file From 14db04910654e5875ae8094f9d6770c1ff3a629b Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 30 Nov 2015 16:32:15 +0100 Subject: [PATCH 066/306] revised TracTracEventManagementPanel --- .../ui/adminconsole/DeviceConfigurationPanel.java | 2 +- .../adminconsole/TracTracEventManagementPanel.java | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java index 95965c7b7ee..8298636a9ec 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java @@ -58,7 +58,7 @@ public class DeviceConfigurationPanel extends SimplePanel implements /*Selection this.errorReporter = reporter; this.refreshableMultiSelectionModel = new RefreshableMultiSelectionModel<>(); - //TODO Create an appropriate EntityIdentityComparator or define an .eqauls() method for DeviceConfigurationMatcherDTO + //TODO create an appropriate EntityIdentityComparator or define an .eqauls() method for DeviceConfigurationMatcherDTO refreshableMultiSelectionModel.addSelectionChangeHandler(new Handler() { @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java index 4c1ce8e59e7..e132e3f64a9 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java @@ -46,6 +46,7 @@ import com.sap.sailing.gwt.ui.shared.TracTracRaceRecordDTO; 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.panels.LabeledAbstractFilterablePanel; /** @@ -357,8 +358,17 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { }; raceVisibilityColumn.setSortable(true); - SelectionCheckboxColumn selectionCheckboxColumn = new SelectionCheckboxColumn(tableResources.cellTableStyle().cellTableCheckboxSelected(), - tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { + SelectionCheckboxColumn selectionCheckboxColumn = new SelectionCheckboxColumn( + tableResources.cellTableStyle().cellTableCheckboxSelected(), + tableResources.cellTableStyle().cellTableCheckboxDeselected(), + tableResources.cellTableStyle().cellTableCheckboxColumnCell(), + new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(TracTracRaceRecordDTO dto1, TracTracRaceRecordDTO dto2) { + return dto1.id.equals(dto2.id); + } + }) { @Override public Boolean getValue(TracTracRaceRecordDTO row) { return racesTable.getSelectionModel().isSelected(row); From 91185c45567d2e6aa33d678087fb5f41da7262a5 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Tue, 1 Dec 2015 08:41:17 +0100 Subject: [PATCH 067/306] revised SwissTimingPanels --- .../SwissTimingEventManagementPanel.java | 13 +++++++++++-- .../SwissTimingReplayConnectorPanel.java | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java index b4c00af416e..1fa1c0b58bc 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java @@ -30,7 +30,6 @@ import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.MultiSelectionModel; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.RegattaName; import com.sap.sailing.gwt.ui.client.RegattaRefresher; @@ -42,6 +41,8 @@ import com.sap.sailing.gwt.ui.shared.SwissTimingEventRecordDTO; import com.sap.sailing.gwt.ui.shared.SwissTimingRaceRecordDTO; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; /** @@ -261,7 +262,15 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane raceTable.addColumn(genderColumn, "Gender"); raceTable.addColumn(raceStartTimeColumn, stringConstants.startTime()); raceTable.setWidth("300px"); - raceTable.setSelectionModel(new MultiSelectionModel() {}); + raceTable.setSelectionModel(new RefreshableMultiSelectionModel( + new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(SwissTimingRaceRecordDTO dto1, SwissTimingRaceRecordDTO dto2) { + return dto1.raceId.equals(dto2.raceId); + } + }) { + }); trackableRacesPanel.add(raceTable); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java index 69e335d4ab3..4e072330130 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java @@ -30,7 +30,6 @@ import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.MultiSelectionModel; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.RegattaName; import com.sap.sailing.gwt.ui.client.RegattaRefresher; @@ -41,6 +40,8 @@ import com.sap.sailing.gwt.ui.shared.SwissTimingArchiveConfigurationDTO; import com.sap.sailing.gwt.ui.shared.SwissTimingReplayRaceDTO; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; /** @@ -189,7 +190,15 @@ public class SwissTimingReplayConnectorPanel extends AbstractEventManagementPane raceTable.addColumn(boatClassNamesColumn, stringMessages.boatClass()); raceTable.addColumn(raceStartTrackingColumn, stringMessages.startTime()); raceTable.setWidth("300px"); - raceTable.setSelectionModel(new MultiSelectionModel() {}); + raceTable.setSelectionModel(new RefreshableMultiSelectionModel( + new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(SwissTimingReplayRaceDTO dto1, SwissTimingReplayRaceDTO dto2) { + return dto1.race_id.equals(dto2.race_id); + } + }) { + }); racesHorizontalPanel.add(raceTable); racesHorizontalPanel.add(trackPanel); From 7c8922e069fef5adbf72d6c372fc9f2d418e0887 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Tue, 1 Dec 2015 09:12:39 +0100 Subject: [PATCH 068/306] revised SmartphoneTrackingPanel --- ...ackFileImportDeviceIdentifierTableWrapper.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java index f99e62eb864..b1aa2255c09 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TrackFileImportDeviceIdentifierTableWrapper.java @@ -1,19 +1,28 @@ package com.sap.sailing.gwt.ui.adminconsole; import com.google.gwt.user.cellview.client.TextColumn; -import com.google.gwt.view.client.SingleSelectionModel; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; import com.sap.sailing.gwt.ui.shared.TrackFileImportDeviceIdentifierDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; public class TrackFileImportDeviceIdentifierTableWrapper extends TableWrapper> { +RefreshableSingleSelectionModel> { public TrackFileImportDeviceIdentifierTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter errorReporter) { - super(sailingService, stringMessages, errorReporter, /* multiSelection */ false, /* enablePager */ true, null /*EntityIdentityComparator for RefreshableSelectionModel*/); + super(sailingService, stringMessages, errorReporter, /* multiSelection */ false, /* enablePager */ true, + new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(TrackFileImportDeviceIdentifierDTO dto1, + TrackFileImportDeviceIdentifierDTO dto2) { + return dto1.uuidAsString.equals(dto2.uuidAsString); + } + }); TextColumn uuidColumn = new TextColumn() { @Override From 666d4f3068dd35b00d0aad44dc8e25e9f7452b56 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Tue, 1 Dec 2015 09:25:10 +0100 Subject: [PATCH 069/306] newSelectionModel: made IgtimiAccountsPanel refreshable --- .../ui/adminconsole/IgtimiAccountsPanel.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java index db01cb84507..7326be8ef8e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java @@ -29,13 +29,13 @@ 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.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.controls.ImagesBarCell; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.IconResources; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -46,6 +46,7 @@ public class IgtimiAccountsPanel extends FlowPanel { private final ErrorReporter errorReporter; private final CellTable allAccounts; private final LabeledAbstractFilterablePanel filterAccountsPanel; + private final RefreshableSingleSelectionModel refreshableAccountsSelectionModel; public static class AccountImagesBarCell extends ImagesBarCell { public static final String ACTION_REMOVE = "ACTION_REMOVE"; @@ -75,8 +76,8 @@ public class IgtimiAccountsPanel extends FlowPanel { AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); allAccounts = new CellTable(/* pageSize */10000, tableRes); - final SingleSelectionModel accountsSelectionModel = new SingleSelectionModel(); - allAccounts.setSelectionModel(accountsSelectionModel); + refreshableAccountsSelectionModel = new RefreshableSingleSelectionModel(); + allAccounts.setSelectionModel(refreshableAccountsSelectionModel); final ListDataProvider filteredAccounts = new ListDataProvider(); ListHandler accountColumnListHandler = new ListHandler(filteredAccounts.getList()); filteredAccounts.addDataDisplay(allAccounts); @@ -94,17 +95,17 @@ public class IgtimiAccountsPanel extends FlowPanel { removeAccountButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { - if (accountsSelectionModel.getSelectedObject() != null) { + if (refreshableAccountsSelectionModel.getSelectedObject() != null) { if (Window.confirm("Do you really want to remove the leaderboards?")) { - removeAccount(accountsSelectionModel.getSelectedObject(), filteredAccounts); + removeAccount(refreshableAccountsSelectionModel.getSelectedObject(), filteredAccounts); } } } }); - accountsSelectionModel.addSelectionChangeHandler(new Handler() { + refreshableAccountsSelectionModel.addSelectionChangeHandler(new Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { - removeAccountButton.setEnabled(accountsSelectionModel.getSelectedObject() != null); + removeAccountButton.setEnabled(refreshableAccountsSelectionModel.getSelectedObject() != null); } }); controlsPanel.add(filterAccountsPanel); @@ -250,7 +251,7 @@ public class IgtimiAccountsPanel extends FlowPanel { return new UserData(eMail.getText(), password.getText()); } } - + private static void updateAllAccounts(SailingServiceAsync sailingService, final LabeledAbstractFilterablePanel filterAccountsPanel, final StringMessages stringMessages, final ErrorReporter errorReporter) { sailingService.getAllIgtimiAccountEmailAddresses(new AsyncCallback>() { From ec45416b77e65ffa07bdc8fbcb1f68c689fd3ceb Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Tue, 1 Dec 2015 10:03:09 +0100 Subject: [PATCH 070/306] newSelctionModel: made RemoteServerInstancesManagementPanel refreshable --- .../RemoteServerInstancesManagementPanel.java | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java index 70a30fdf3ba..29df3c012a5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java @@ -25,12 +25,13 @@ import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.MultiSelectionModel; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.EventBaseDTO; import com.sap.sailing.gwt.ui.shared.RemoteSailingServerReferenceDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -40,7 +41,7 @@ public class RemoteServerInstancesManagementPanel extends SimplePanel { private final StringMessages stringMessages; private final AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); - private MultiSelectionModel serverSelectionModel; + private RefreshableMultiSelectionModel refreshableServerSelectionModel; private LabeledAbstractFilterablePanel filteredServerTablePanel; private final CaptionPanel remoteServersPanel; @@ -158,8 +159,16 @@ public class RemoteServerInstancesManagementPanel extends SimplePanel { serverTable.setEmptyTableWidget(new Label(stringMessages.noSailingServerInstancesYet())); - serverSelectionModel = new MultiSelectionModel(); - serverTable.setSelectionModel(serverSelectionModel); + refreshableServerSelectionModel = new RefreshableMultiSelectionModel( + new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(RemoteSailingServerReferenceDTO dto1, + RemoteSailingServerReferenceDTO dto2) { + return dto1.getUrl().equals(dto2.getUrl()); + } + }); + serverTable.setSelectionModel(refreshableServerSelectionModel); return serverTable; } @@ -174,13 +183,14 @@ public class RemoteServerInstancesManagementPanel extends SimplePanel { @Override public void onSuccess(List result) { filteredServerTablePanel.updateAll(result); + refreshableServerSelectionModel.refreshSelectionModel(result); } }); } private void removeSelectedSailingServers() { Set toRemove = new HashSet(); - for (RemoteSailingServerReferenceDTO selectedServer: serverSelectionModel.getSelectedSet()) { + for (RemoteSailingServerReferenceDTO selectedServer: refreshableServerSelectionModel.getSelectedSet()) { toRemove.add(selectedServer.getName()); } From 465de8dda217bb4bf63f49dc3485181a1fe9b73d Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 1 Dec 2015 11:34:36 +0100 Subject: [PATCH 071/306] Adjusted leaderboard calculation logic for meta leaderboard to show current act as live in overall leaderboard view --- .../common/dto/AbstractLeaderboardDTO.java | 863 ++-- .../dto/MetaLeaderboardRaceColumnDTO.java | 49 + .../common/dto/RaceColumnDTOFactory.java | 10 +- .../impl/AbstractSimpleLeaderboardImpl.java | 3839 +++++++++-------- .../gwt/ui/test/TestColumnSwapping.java | 324 +- .../gwt/ui/server/SailingServiceImpl.java | 6 +- 6 files changed, 2586 insertions(+), 2505 deletions(-) create mode 100644 java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java index 8a9fa9017a9..779d835d434 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java @@ -1,430 +1,433 @@ -package com.sap.sailing.domain.common.dto; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import com.sap.sailing.domain.common.LeaderboardType; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sse.common.Util; - -public abstract class AbstractLeaderboardDTO implements Serializable { - private static final long serialVersionUID = -205106531931903527L; - - public String name; - - private List races; - public Map competitorDisplayNames; - public Map rows; - public boolean hasCarriedPoints; - public int[] discardThresholds; - - /** - * Set to the non-null regatta name if this DTO represents a RegattaLeaderboard. - */ - public String regattaName; - public String displayName; - public UUID defaultCourseAreaId; - public String defaultCourseAreaName; - public ScoringSchemeType scoringScheme; - public LeaderboardType type; - - private Long delayToLiveInMillisForLatestRace; - - public AbstractLeaderboardDTO() { - races = new ArrayList(); - } - - public Set getBoatClasses() { - Set result = new HashSet(); - if(rows != null) { - for (CompetitorDTO competitor : rows.keySet()) { - result.add(competitor.getBoatClass()); - } - } - return result; - } - - public String getDisplayName() { - return displayName; - } - - public String getDisplayName(CompetitorDTO competitor) { - if (competitorDisplayNames == null || competitorDisplayNames.get(competitor) == null) { - return competitor.getName(); - } else { - return competitorDisplayNames.get(competitor); - } - } - - /** - * If the race column whose name is specified in raceColumnName has at least one competitor who has valid - * {@link LeaderboardEntryDTO#legDetails} for that race column, the maximum number of entries of all such competitors in - * the leg details is returned, telling the number of legs that the race column shall display. Otherwise, -1 is returned. - * If you specify an non null preselectedRace the leg count is calculated only for that race for the column - * holding that race. All other columns will be unaffected by preselectedRace and will return their regular - * leg count. Note that this may lead to a situation where some competitors may have sailed more legs than others because - * their fleet association may have varied across races.

    - * - * See also bug 2604 and bug 2035. - */ - public int getLegCount(String raceColumnName, RaceIdentifier preselectedRace) { - int result = -1; - final boolean preselectedRaceIsInRaceColumn = isRaceInColumn(preselectedRace, raceColumnName); - for (LeaderboardRowDTO row : rows.values()) { - LeaderboardEntryDTO leaderboardEntryDTO = row.fieldsByRaceColumnName.get(raceColumnName); - if (leaderboardEntryDTO != null && leaderboardEntryDTO.legDetails != null - // when no race is pre-selected, always use the actual leg count; - // otherwise, if the pre-selected race is in the column identified by - // raceColumnName, use the entry only if it's in the pre-selected race; - // otherwise (a race is pre-selected but a different column's leg count - // is requested), use the regular leg count as is. - && (preselectedRace == null || !preselectedRaceIsInRaceColumn || preselectedRace.equals(leaderboardEntryDTO.race))) { - result = Math.max(result, leaderboardEntryDTO.legDetails.size()); - } - } - return result; - } - - private boolean isRaceInColumn(RaceIdentifier preselectedRace, String raceColumnName) { - RaceColumnDTO raceColumn = getRaceColumnByName(raceColumnName); - return raceColumn.containsRace(preselectedRace); - } - - /** - * Tells if the competitor scored (and therefore presumably participated) in a medal race represented - * in this leaderboard. - */ - public boolean scoredInMedalRace(CompetitorDTO competitor) { - LeaderboardRowDTO row = rows.get(competitor); - for (RaceColumnDTO race : races) { - if (race.isMedalRace() && row.fieldsByRaceColumnName.get(race.getRaceColumnName()).totalPoints > 0) { - return true; - } - } - return false; - } - - public Double getNetPoints(CompetitorDTO competitor, String nameOfLastRaceSoFar) { - Double netPoints = null; - LeaderboardRowDTO row = rows.get(competitor); - if (row != null) { - LeaderboardEntryDTO field = row.fieldsByRaceColumnName.get(nameOfLastRaceSoFar); - if (field != null && field.netPoints != null) { - netPoints = field.netPoints; - } - } - return netPoints; - } - - public boolean raceIsTracked(String raceColumnName) { - for (RaceColumnDTO race : races) { - if (race.getRaceColumnName().equals(raceColumnName)) { - for (FleetDTO fleet : race.getFleets()) { - if (race.isTrackedRace(fleet)) { - return true; - } - } - } - } - return false; - - } - - public boolean raceIsMedalRace(String raceColumnName) { - return getRaceColumnByName(raceColumnName).isMedalRace(); - } - - /** - * If the {@link RaceColumnDTO} by the name raceColumnName doesn't exist yet within this leaderboard - * DTO, it is created, setting is {@link RaceColumnDTO#isValidInTotalScore()} to true. This method - * ensures that a fleet named fleetName is present. If it's not present yet, it's added to the race - * column's fleet name list. The trackedRaceIdentifier and race are associated with the - * column for the fleet identified by fleetName. - * - * @param explicitFactor - * factor by which to multiply the race column's points for the overall score; if null, the - * default will be determined by whether or not the column is marked as medal race - * @param effectiveFactor - * is what you get when you call {@link RaceColumn#getFactor()} on the race column that the resulting - * {@link RaceColumnDTO} represents - * @param regattaName - * must not be null if seriesName is not null; specified - * separately from trackedRaceIdentifier because a column may belong to regatta/series - * despite not having a tracked race associated - * @param seriesName - * if null, this method will produce a {@link RaceColumnDTO}, otherwise a - * {@link RaceColumnInSeriesDTO} - * @param fleetDTO - * must not be null - */ - public RaceColumnDTO addRace(String raceColumnName, Double explicitFactor, double effectiveFactor, - String regattaName, String seriesName, FleetDTO fleetDTO, boolean medalRace, RegattaAndRaceIdentifier trackedRaceIdentifier, RaceDTO race) { - assert fleetDTO != null; - RaceColumnDTO raceColumnDTO = getRaceColumnByName(raceColumnName); - if (raceColumnDTO == null) { - raceColumnDTO = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, medalRace, - explicitFactor, regattaName, seriesName); - races.add(raceColumnDTO); - } - raceColumnDTO.setEffectiveFactor(effectiveFactor); - boolean contains = false; - for (FleetDTO fleet : raceColumnDTO.getFleets()) { - if (fleet.getName().equals(fleetDTO.getName())) { - contains = true; - break; - } - } - if (!contains) { - raceColumnDTO.addFleet(fleetDTO); - } - raceColumnDTO.setRaceIdentifier(fleetDTO, trackedRaceIdentifier); - raceColumnDTO.setRace(fleetDTO, race); - return raceColumnDTO; - } - - public RaceColumnDTO createEmptyRaceColumn(String raceColumnName, boolean medalRace, String regattaName, String seriesName) { - final RaceColumnDTO raceColumn = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, - medalRace, /* explicit factor */ null, regattaName, seriesName); - races.add(raceColumn); - return raceColumn; - } - - public RaceColumnDTO getRaceColumnByName(String raceColumnName) { - for (RaceColumnDTO race : races) { - if (race.getRaceColumnName().equals(raceColumnName)) { - return race; - } - } - return null; - } - - public int getRaceColumnsCount() { - return races.size(); - } - - public int getRacesCount() { - int result = 0; - for (RaceColumnDTO race : getRaceList()) { - result += race.getFleets().size(); - } - return result; - } - - public int getTrackedRacesCount() { - int result = 0; - for (RaceColumnDTO raceColumn : getRaceList()) { - for (FleetDTO fleet : raceColumn.getFleets()) { - RaceDTO race = raceColumn.getRace(fleet); - if(race != null && race.trackedRace != null && race.trackedRace.hasGPSData && race.trackedRace.hasWindData) { - result++; - } - } - } - return result; - } - - public List getRaceList() { - return races; - } - - public void setRaceList(List raceList) { - this.races = raceList; - } - - public boolean raceListContains(String raceColumnName) { - return getRaceColumnByName(raceColumnName) != null; - } - - public void moveRaceUp(String raceColumnName) { - RaceColumnDTO race = getRaceColumnByName(raceColumnName); - int index = races.indexOf(race); - index--; - if (index >= 0) { - races.remove(index + 1); - races.add(index, race); - } - } - - public void moveRaceDown(String raceColumnName) { - RaceColumnDTO race = getRaceColumnByName(raceColumnName); - int index = races.indexOf(race); - if (index != -1) { - index++; - if (index < races.size()) { - races.remove(index - 1); - races.add(index, race); - } - } - } - - public void setIsMedalRace(String raceColumnName, boolean medalRace) { - getRaceColumnByName(raceColumnName).setMedalRace(medalRace); - } - - /** - * @return The earliest start date of the races, or null if no start dates of the races are available. - */ - public Date getStartDate() { - Date leaderboardStart = null; - for (RaceColumnDTO race : getRaceList()) { - for (FleetDTO fleet: race.getFleets()) { - Date raceStart = race.getStartDate(fleet); - if (raceStart != null) { - if (leaderboardStart == null) { - leaderboardStart = new Date(); - } else { - leaderboardStart = leaderboardStart.before(raceStart) ? leaderboardStart : raceStart; - } - } - } - } - return leaderboardStart; - } - - /** - * Takes the {@link PlacemarkOrderDTO} of all races in this leaderboard, if the PlacemarkOrderDTO for the race is - * available, and fills all {@link PlacemarkDTO} in a new PlacemarkOrderDTO.
    - * The order of the races in this leaderboard determine the order of the PlacemarkDTOs in the PlacemarkOrderDTO. - * - * @return The places of this leaderboard in form of a {@link PlacemarkOrderDTO}, or null if the - * {@link PlacemarkOrderDTO places} of no race are available - */ - public PlacemarkOrderDTO getPlaces() { - PlacemarkOrderDTO leaderboardPlaces = null; - for (RaceColumnDTO race : getRaceList()) { - PlacemarkOrderDTO racePlaces = race.getPlaces(); - if (racePlaces != null) { - if (leaderboardPlaces == null) { - leaderboardPlaces = new PlacemarkOrderDTO(); - } - leaderboardPlaces.getPlacemarks().addAll(racePlaces.getPlacemarks()); - } - } - return leaderboardPlaces; - } - - /** - * @return true if the leaderboard contains a race which is live - */ - public boolean hasLiveRace(long serverTimePointAsMillis) { - for (RaceColumnDTO race : getRaceList()) { - for (FleetDTO fleet : race.getFleets()) { - if (race.isLive(fleet, serverTimePointAsMillis)) { - return true; - } - } - } - return false; - } - - public List> getLiveRaces(long serverTimePointAsMillis) { - List> result = new ArrayList>(); - for (RaceColumnDTO race : getRaceList()) { - for (FleetDTO fleet : race.getFleets()) { - if (race.isLive(fleet, serverTimePointAsMillis)) { - result.add(new Util.Pair(race, fleet)); - } - } - } - return result; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((competitorDisplayNames == null) ? 0 : competitorDisplayNames.hashCode()); - result = prime * result + Arrays.hashCode(discardThresholds); - result = prime * result + (hasCarriedPoints ? 1231 : 1237); - result = prime * result + ((name == null) ? 0 : name.hashCode()); - result = prime * result + ((scoringScheme == null) ? 0 : scoringScheme.hashCode()); - if (races == null) { - result = prime * result; - } else { - List raceNames = new ArrayList(); - for (RaceColumnDTO race : races) { - raceNames.add(race.getName()); - } - result = prime * result + raceNames.hashCode(); - } - result = prime * result + ((rows == null) ? 0 : rows.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - AbstractLeaderboardDTO other = (AbstractLeaderboardDTO) obj; - if (competitorDisplayNames == null) { - if (other.competitorDisplayNames != null) - return false; - } else if (!competitorDisplayNames.equals(other.competitorDisplayNames)) - return false; - if (!Arrays.equals(discardThresholds, other.discardThresholds)) - return false; - if (hasCarriedPoints != other.hasCarriedPoints) - return false; - if (type != other.type) - return false; - if (scoringScheme != other.scoringScheme) - return false; - if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; - if (races == null) { - if (other.races != null) - return false; - } else { - // compare race column names only, not the contents - if (races.size() != (other.races==null?0:other.races.size())) { - return false; - } - List raceColumnNames = new ArrayList(races.size()); - List otherRaceColumnNames = new ArrayList(races.size()); - for (RaceColumnDTO race : races) { - raceColumnNames.add(race.getName()); - } - if (other.races != null) { - for (RaceColumnDTO otherRace : other.races) { - otherRaceColumnNames.add(otherRace.getName()); - } - } - if (!raceColumnNames.equals(otherRaceColumnNames)) - return false; - if (rows == null) { - if (other.rows != null) - return false; - } else if (!rows.equals(other.rows)) - return false; - } - return true; - } - - public boolean isDisplayNameSet(CompetitorDTO competitor) { - return competitorDisplayNames.get(competitor) != null; - } - - public Long getDelayToLiveInMillisForLatestRace() { - return delayToLiveInMillisForLatestRace; - } - - public void setDelayToLiveInMillisForLatestRace(Long delayToLiveInMillisForLatestRace) { - this.delayToLiveInMillisForLatestRace = delayToLiveInMillisForLatestRace; - } -} +package com.sap.sailing.domain.common.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sse.common.Util; + +public abstract class AbstractLeaderboardDTO implements Serializable { + private static final long serialVersionUID = -205106531931903527L; + + public String name; + + private List races; + public Map competitorDisplayNames; + public Map rows; + public boolean hasCarriedPoints; + public int[] discardThresholds; + + /** + * Set to the non-null regatta name if this DTO represents a RegattaLeaderboard. + */ + public String regattaName; + public String displayName; + public UUID defaultCourseAreaId; + public String defaultCourseAreaName; + public ScoringSchemeType scoringScheme; + public LeaderboardType type; + + private Long delayToLiveInMillisForLatestRace; + + public AbstractLeaderboardDTO() { + races = new ArrayList(); + } + + public Set getBoatClasses() { + Set result = new HashSet(); + if(rows != null) { + for (CompetitorDTO competitor : rows.keySet()) { + result.add(competitor.getBoatClass()); + } + } + return result; + } + + public String getDisplayName() { + return displayName; + } + + public String getDisplayName(CompetitorDTO competitor) { + if (competitorDisplayNames == null || competitorDisplayNames.get(competitor) == null) { + return competitor.getName(); + } else { + return competitorDisplayNames.get(competitor); + } + } + + /** + * If the race column whose name is specified in raceColumnName has at least one competitor who has valid + * {@link LeaderboardEntryDTO#legDetails} for that race column, the maximum number of entries of all such competitors in + * the leg details is returned, telling the number of legs that the race column shall display. Otherwise, -1 is returned. + * If you specify an non null preselectedRace the leg count is calculated only for that race for the column + * holding that race. All other columns will be unaffected by preselectedRace and will return their regular + * leg count. Note that this may lead to a situation where some competitors may have sailed more legs than others because + * their fleet association may have varied across races.

    + * + * See also bug 2604 and bug 2035. + */ + public int getLegCount(String raceColumnName, RaceIdentifier preselectedRace) { + int result = -1; + final boolean preselectedRaceIsInRaceColumn = isRaceInColumn(preselectedRace, raceColumnName); + for (LeaderboardRowDTO row : rows.values()) { + LeaderboardEntryDTO leaderboardEntryDTO = row.fieldsByRaceColumnName.get(raceColumnName); + if (leaderboardEntryDTO != null && leaderboardEntryDTO.legDetails != null + // when no race is pre-selected, always use the actual leg count; + // otherwise, if the pre-selected race is in the column identified by + // raceColumnName, use the entry only if it's in the pre-selected race; + // otherwise (a race is pre-selected but a different column's leg count + // is requested), use the regular leg count as is. + && (preselectedRace == null || !preselectedRaceIsInRaceColumn || preselectedRace.equals(leaderboardEntryDTO.race))) { + result = Math.max(result, leaderboardEntryDTO.legDetails.size()); + } + } + return result; + } + + private boolean isRaceInColumn(RaceIdentifier preselectedRace, String raceColumnName) { + RaceColumnDTO raceColumn = getRaceColumnByName(raceColumnName); + return raceColumn.containsRace(preselectedRace); + } + + /** + * Tells if the competitor scored (and therefore presumably participated) in a medal race represented + * in this leaderboard. + */ + public boolean scoredInMedalRace(CompetitorDTO competitor) { + LeaderboardRowDTO row = rows.get(competitor); + for (RaceColumnDTO race : races) { + if (race.isMedalRace() && row.fieldsByRaceColumnName.get(race.getRaceColumnName()).totalPoints > 0) { + return true; + } + } + return false; + } + + public Double getNetPoints(CompetitorDTO competitor, String nameOfLastRaceSoFar) { + Double netPoints = null; + LeaderboardRowDTO row = rows.get(competitor); + if (row != null) { + LeaderboardEntryDTO field = row.fieldsByRaceColumnName.get(nameOfLastRaceSoFar); + if (field != null && field.netPoints != null) { + netPoints = field.netPoints; + } + } + return netPoints; + } + + public boolean raceIsTracked(String raceColumnName) { + for (RaceColumnDTO race : races) { + if (race.getRaceColumnName().equals(raceColumnName)) { + for (FleetDTO fleet : race.getFleets()) { + if (race.isTrackedRace(fleet)) { + return true; + } + } + } + } + return false; + + } + + public boolean raceIsMedalRace(String raceColumnName) { + return getRaceColumnByName(raceColumnName).isMedalRace(); + } + + /** + * If the {@link RaceColumnDTO} by the name raceColumnName doesn't exist yet within this leaderboard + * DTO, it is created, setting is {@link RaceColumnDTO#isValidInTotalScore()} to true. This method + * ensures that a fleet named fleetName is present. If it's not present yet, it's added to the race + * column's fleet name list. The trackedRaceIdentifier and race are associated with the + * column for the fleet identified by fleetName. + * + * @param explicitFactor + * factor by which to multiply the race column's points for the overall score; if null, the + * default will be determined by whether or not the column is marked as medal race + * @param effectiveFactor + * is what you get when you call {@link RaceColumn#getFactor()} on the race column that the resulting + * {@link RaceColumnDTO} represents + * @param regattaName + * must not be null if seriesName is not null; specified + * separately from trackedRaceIdentifier because a column may belong to regatta/series + * despite not having a tracked race associated + * @param seriesName + * if null, this method will produce a {@link RaceColumnDTO}, otherwise a + * {@link RaceColumnInSeriesDTO} + * @param fleetDTO + * must not be null + * @param isMetaLeaderboardColumn + */ + public RaceColumnDTO addRace(String raceColumnName, Double explicitFactor, double effectiveFactor, + String regattaName, String seriesName, FleetDTO fleetDTO, boolean medalRace, + RegattaAndRaceIdentifier trackedRaceIdentifier, RaceDTO race, boolean isMetaLeaderboardColumn) { + assert fleetDTO != null; + RaceColumnDTO raceColumnDTO = getRaceColumnByName(raceColumnName); + if (raceColumnDTO == null) { + raceColumnDTO = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, medalRace, + explicitFactor, regattaName, seriesName, isMetaLeaderboardColumn); + races.add(raceColumnDTO); + } + raceColumnDTO.setEffectiveFactor(effectiveFactor); + boolean contains = false; + for (FleetDTO fleet : raceColumnDTO.getFleets()) { + if (fleet.getName().equals(fleetDTO.getName())) { + contains = true; + break; + } + } + if (!contains) { + raceColumnDTO.addFleet(fleetDTO); + } + raceColumnDTO.setRaceIdentifier(fleetDTO, trackedRaceIdentifier); + raceColumnDTO.setRace(fleetDTO, race); + return raceColumnDTO; + } + + public RaceColumnDTO createEmptyRaceColumn(String raceColumnName, boolean medalRace, String regattaName, + String seriesName, boolean isMetaLeaderboardColumn) { + final RaceColumnDTO raceColumn = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, + medalRace, /* explicit factor */ null, regattaName, seriesName, isMetaLeaderboardColumn); + races.add(raceColumn); + return raceColumn; + } + + public RaceColumnDTO getRaceColumnByName(String raceColumnName) { + for (RaceColumnDTO race : races) { + if (race.getRaceColumnName().equals(raceColumnName)) { + return race; + } + } + return null; + } + + public int getRaceColumnsCount() { + return races.size(); + } + + public int getRacesCount() { + int result = 0; + for (RaceColumnDTO race : getRaceList()) { + result += race.getFleets().size(); + } + return result; + } + + public int getTrackedRacesCount() { + int result = 0; + for (RaceColumnDTO raceColumn : getRaceList()) { + for (FleetDTO fleet : raceColumn.getFleets()) { + RaceDTO race = raceColumn.getRace(fleet); + if(race != null && race.trackedRace != null && race.trackedRace.hasGPSData && race.trackedRace.hasWindData) { + result++; + } + } + } + return result; + } + + public List getRaceList() { + return races; + } + + public void setRaceList(List raceList) { + this.races = raceList; + } + + public boolean raceListContains(String raceColumnName) { + return getRaceColumnByName(raceColumnName) != null; + } + + public void moveRaceUp(String raceColumnName) { + RaceColumnDTO race = getRaceColumnByName(raceColumnName); + int index = races.indexOf(race); + index--; + if (index >= 0) { + races.remove(index + 1); + races.add(index, race); + } + } + + public void moveRaceDown(String raceColumnName) { + RaceColumnDTO race = getRaceColumnByName(raceColumnName); + int index = races.indexOf(race); + if (index != -1) { + index++; + if (index < races.size()) { + races.remove(index - 1); + races.add(index, race); + } + } + } + + public void setIsMedalRace(String raceColumnName, boolean medalRace) { + getRaceColumnByName(raceColumnName).setMedalRace(medalRace); + } + + /** + * @return The earliest start date of the races, or null if no start dates of the races are available. + */ + public Date getStartDate() { + Date leaderboardStart = null; + for (RaceColumnDTO race : getRaceList()) { + for (FleetDTO fleet: race.getFleets()) { + Date raceStart = race.getStartDate(fleet); + if (raceStart != null) { + if (leaderboardStart == null) { + leaderboardStart = new Date(); + } else { + leaderboardStart = leaderboardStart.before(raceStart) ? leaderboardStart : raceStart; + } + } + } + } + return leaderboardStart; + } + + /** + * Takes the {@link PlacemarkOrderDTO} of all races in this leaderboard, if the PlacemarkOrderDTO for the race is + * available, and fills all {@link PlacemarkDTO} in a new PlacemarkOrderDTO.
    + * The order of the races in this leaderboard determine the order of the PlacemarkDTOs in the PlacemarkOrderDTO. + * + * @return The places of this leaderboard in form of a {@link PlacemarkOrderDTO}, or null if the + * {@link PlacemarkOrderDTO places} of no race are available + */ + public PlacemarkOrderDTO getPlaces() { + PlacemarkOrderDTO leaderboardPlaces = null; + for (RaceColumnDTO race : getRaceList()) { + PlacemarkOrderDTO racePlaces = race.getPlaces(); + if (racePlaces != null) { + if (leaderboardPlaces == null) { + leaderboardPlaces = new PlacemarkOrderDTO(); + } + leaderboardPlaces.getPlacemarks().addAll(racePlaces.getPlacemarks()); + } + } + return leaderboardPlaces; + } + + /** + * @return true if the leaderboard contains a race which is live + */ + public boolean hasLiveRace(long serverTimePointAsMillis) { + for (RaceColumnDTO race : getRaceList()) { + for (FleetDTO fleet : race.getFleets()) { + if (race.isLive(fleet, serverTimePointAsMillis)) { + return true; + } + } + } + return false; + } + + public List> getLiveRaces(long serverTimePointAsMillis) { + List> result = new ArrayList>(); + for (RaceColumnDTO race : getRaceList()) { + for (FleetDTO fleet : race.getFleets()) { + if (race.isLive(fleet, serverTimePointAsMillis)) { + result.add(new Util.Pair(race, fleet)); + } + } + } + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((competitorDisplayNames == null) ? 0 : competitorDisplayNames.hashCode()); + result = prime * result + Arrays.hashCode(discardThresholds); + result = prime * result + (hasCarriedPoints ? 1231 : 1237); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((scoringScheme == null) ? 0 : scoringScheme.hashCode()); + if (races == null) { + result = prime * result; + } else { + List raceNames = new ArrayList(); + for (RaceColumnDTO race : races) { + raceNames.add(race.getName()); + } + result = prime * result + raceNames.hashCode(); + } + result = prime * result + ((rows == null) ? 0 : rows.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AbstractLeaderboardDTO other = (AbstractLeaderboardDTO) obj; + if (competitorDisplayNames == null) { + if (other.competitorDisplayNames != null) + return false; + } else if (!competitorDisplayNames.equals(other.competitorDisplayNames)) + return false; + if (!Arrays.equals(discardThresholds, other.discardThresholds)) + return false; + if (hasCarriedPoints != other.hasCarriedPoints) + return false; + if (type != other.type) + return false; + if (scoringScheme != other.scoringScheme) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (races == null) { + if (other.races != null) + return false; + } else { + // compare race column names only, not the contents + if (races.size() != (other.races==null?0:other.races.size())) { + return false; + } + List raceColumnNames = new ArrayList(races.size()); + List otherRaceColumnNames = new ArrayList(races.size()); + for (RaceColumnDTO race : races) { + raceColumnNames.add(race.getName()); + } + if (other.races != null) { + for (RaceColumnDTO otherRace : other.races) { + otherRaceColumnNames.add(otherRace.getName()); + } + } + if (!raceColumnNames.equals(otherRaceColumnNames)) + return false; + if (rows == null) { + if (other.rows != null) + return false; + } else if (!rows.equals(other.rows)) + return false; + } + return true; + } + + public boolean isDisplayNameSet(CompetitorDTO competitor) { + return competitorDisplayNames.get(competitor) != null; + } + + public Long getDelayToLiveInMillisForLatestRace() { + return delayToLiveInMillisForLatestRace; + } + + public void setDelayToLiveInMillisForLatestRace(Long delayToLiveInMillisForLatestRace) { + this.delayToLiveInMillisForLatestRace = delayToLiveInMillisForLatestRace; + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java new file mode 100644 index 00000000000..16cc926d0e0 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java @@ -0,0 +1,49 @@ +package com.sap.sailing.domain.common.dto; + +import java.util.ArrayList; + +public class MetaLeaderboardRaceColumnDTO extends RaceColumnDTO { + + private static final long serialVersionUID = 6152752963316150432L; + + private ArrayList raceList = new ArrayList(); + + public boolean isLive(FleetDTO fleet, long serverTimePointAsMillis) { + for (RaceDTO race : raceList) { + if (race.isLive(serverTimePointAsMillis)) { + return true; + } + } + return false; + } + + public void addRace(RaceDTO race) { + raceList.add(race); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((raceList == null) ? 0 : raceList.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + MetaLeaderboardRaceColumnDTO other = (MetaLeaderboardRaceColumnDTO) obj; + if (raceList == null) { + if (other.raceList != null) + return false; + } else if (!raceList.equals(other.raceList)) + return false; + return true; + } + +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java index 7d8a0efbb67..46083549e93 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java @@ -7,15 +7,17 @@ public class RaceColumnDTOFactory { * @param regattaName * must not be null if seriesName is not null * @param seriesName - * when null, a regular {@link RaceColumnDTO} will be produced and initialized from the - * properties passed; otherwise, a {@link RaceColumnInSeriesDTO} will be created. + * when null, a regular {@link RaceColumnDTO} or {@link MetaLeaderboardRaceColumnDTO} will + * be produced (depending on isMetaLeaderboardColumn) and initialized from the properties + * passed; otherwise, a {@link RaceColumnInSeriesDTO} will be created. */ - public RaceColumnDTO createRaceColumnDTO(String columnName, boolean isMedal, Double explicitFactor, String regattaName, String seriesName) { + public RaceColumnDTO createRaceColumnDTO(String columnName, boolean isMedal, Double explicitFactor, + String regattaName, String seriesName, boolean isMetaLeaderboardColumn) { final RaceColumnDTO raceColumnDTO; if (seriesName != null) { raceColumnDTO = new RaceColumnInSeriesDTO(seriesName, regattaName); } else { - raceColumnDTO = new RaceColumnDTO(); + raceColumnDTO = isMetaLeaderboardColumn ? new MetaLeaderboardRaceColumnDTO() : new RaceColumnDTO(); } fillRaceColumnDTO(raceColumnDTO, columnName, isMedal, explicitFactor); return raceColumnDTO; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java index de92f254836..bdf437fe905 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java @@ -1,1907 +1,1932 @@ -package com.sap.sailing.domain.leaderboard.impl; - -import java.io.IOException; -import java.io.ObjectInputStream; -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.NavigableSet; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RunnableFuture; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.sap.sailing.domain.abstractlog.race.InvalidatesLeaderboardCache; -import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.Course; -import com.sap.sailing.domain.base.DomainFactory; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Leg; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnInSeries; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.LegType; -import com.sap.sailing.domain.common.ManeuverType; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.NoWindError; -import com.sap.sailing.domain.common.NoWindException; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.dto.FleetDTO; -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.RaceDTO; -import com.sap.sailing.domain.common.tracking.GPSFixMoving; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.NumberOfCompetitorsInLeaderboardFetcher; -import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.ScoreCorrection.Result; -import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCache; -import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; -import com.sap.sailing.domain.leaderboard.caching.LiveLeaderboardUpdater; -import com.sap.sailing.domain.racelog.RaceLogIdentifier; -import com.sap.sailing.domain.ranking.RankingMetric.CompetitorRankingInfo; -import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; -import com.sap.sailing.domain.tracking.GPSFixTrack; -import com.sap.sailing.domain.tracking.Maneuver; -import com.sap.sailing.domain.tracking.MarkPassing; -import com.sap.sailing.domain.tracking.MarkPassingManeuver; -import com.sap.sailing.domain.tracking.RaceChangeListener; -import com.sap.sailing.domain.tracking.TrackedLeg; -import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; -import com.sap.sailing.domain.tracking.WindLegTypeAndLegBearingCache; -import com.sap.sailing.domain.tracking.WindPositionMode; -import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; -import com.sap.sailing.util.impl.RaceColumnListeners; -import com.sap.sse.common.Duration; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.Util; -import com.sap.sse.common.impl.MillisecondsTimePoint; -import com.sap.sse.concurrent.LockUtil; -import com.sap.sse.concurrent.NamedReentrantReadWriteLock; -import com.sap.sse.util.impl.ThreadFactoryWithPriority; - -/** - * Base implementation for various types of leaderboards. The {@link RaceColumnListener} implementation forwards events - * received to all {@link RaceColumnListener} subscribed with this leaderboard. To which objects this leaderboard subscribes - * as {@link RaceColumnListener} is left to the concrete subclasses to implement, but the race columns seem like useful - * candidates. - * - * @author Axel Uhl (d043530) - * - */ -public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, RaceColumnListener { - private static final long serialVersionUID = 330156778603279333L; - - private static final Logger logger = Logger.getLogger(AbstractSimpleLeaderboardImpl.class.getName()); - - static final Double DOUBLE_0 = new Double(0); - - private final SettableScoreCorrection scoreCorrection; - - private ThresholdBasedResultDiscardingRule crossLeaderboardResultDiscardingRule; - - /** - * The optional display name mappings for competitors. This allows a user to override the tracking-provided - * competitor names for display in a leaderboard. - */ - private final Map displayNames; - - /** the display name of the leaderboard */ - private String displayName; - - /** - * Backs the {@link #getCarriedPoints(Competitor)} API with data. Can be used to prime this leaderboard - * with aggregated results of races not tracked / displayed by this leaderboard in detail. The points - * provided by this map are considered by {@link #getTotalPoints(Competitor, TimePoint)}. - */ - private final Map carriedPoints; - - private final RaceColumnListeners raceColumnListeners; - - /** - * A set that manages the difference between {@link #getCompetitors()} and {@link #getAllCompetitors()}. Access - * is controlled by the {@link #suppressedCompetitorsLock} lock. - */ - private final Set suppressedCompetitors; - private final NamedReentrantReadWriteLock suppressedCompetitorsLock; - - private transient Map, RunnableFuture> raceDetailsAtEndOfTrackingCache; - - /** - * This executor needs to be a different one than {@link #executor} because the tasks run by {@link #executor} - * can depend on the results of the tasks run by {@link #raceDetailsExecutor}, and an {@link Executor} doesn't - * move a task that is blocked by waiting for another {@link FutureTask} to the side but blocks permanently, - * ending in a deadlock (one that cannot easily be detected by the Eclipse debugger either). - */ - private transient Executor raceDetailsExecutor; - - /** - * Used to remove all these listeners from their tracked races when this servlet is {@link #destroy() destroyed}. - */ - private transient Set cacheInvalidationListeners; - - private transient ThreadPoolExecutor executor; - - private transient LiveLeaderboardUpdater liveLeaderboardUpdater; - - private transient LeaderboardDTOCache leaderboardDTOCache; - - /** - * A leaderboard entry representing a snapshot of a cell at a given time point for a single race/competitor. - * - * @author Axel Uhl (d043530) - * - */ - class EntryImpl implements Entry { - private final Callable trackedRankProvider; - private final Double netPoints; - private final Callable netPointsUncorrectedProvider; - private final boolean isNetPointsCorrected; - private final Double totalPoints; - private final MaxPointsReason maxPointsReason; - private final boolean discarded; - private final Fleet fleet; - - private EntryImpl(Callable trackedRankProvider, Double netPoints, - Callable netPointsUncorrectedProvider, boolean isNetPointsCorrected, - Double totalPoints, MaxPointsReason maxPointsReason, boolean discarded, Fleet fleet) { - super(); - this.trackedRankProvider = trackedRankProvider; - this.netPoints = netPoints; - this.netPointsUncorrectedProvider = netPointsUncorrectedProvider; - this.isNetPointsCorrected = isNetPointsCorrected; - this.totalPoints = totalPoints; - this.maxPointsReason = maxPointsReason; - this.discarded = discarded; - this.fleet = fleet; - } - @Override - public int getTrackedRank() { - try { - return trackedRankProvider.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - @Override - public Double getNetPoints() { - return netPoints; - } - @Override - public boolean isNetPointsCorrected() { - return isNetPointsCorrected; - } - @Override - public Double getTotalPoints() { - return totalPoints; - } - @Override - public MaxPointsReason getMaxPointsReason() { - return maxPointsReason; - } - @Override - public boolean isDiscarded() { - return discarded; - } - @Override - public Fleet getFleet() { - return fleet; - } - @Override - public Double getNetPointsUncorrected() { - try { - return netPointsUncorrectedProvider.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - - /** - * Computing the competitors can be a bit expensive, particularly if the fleet is large and there may be suppressed - * competitors, and the leaderboard may be a meta-leaderboard that refers to other leaderboards which each have - * several tracked races attached from where the competitors need to be retrieved. Ideally, the competitors list - * would be cached, but that is again difficult because we would have to monitor all changes in all dependent - * leaderboards and columns and tracked races properly. - *

    - * - * As it turns out, one of the most frequent uses of the {@link AbstractSimpleLeaderboardImpl#getCompetitors} - * competitors list is to determine their number which in turn is only required for high-point scoring systems and - * for computing the default score for penalties. Again, the most frequently used low-point family of scoring schemes - * does not require this number. Yet, the scoring scheme requires an argument for polymorphic use by those that - * need it. Instead of computing it for each call, this interface lets us defer the actual calculation until the - * point when it's really needed. Once asked, this object will cache the result. Therefore, a new one should be - * constructed each time the number shall be computed. - * - * @author Axel Uhl (D043530) - * - */ - public class NumberOfCompetitorsFetcherImpl implements NumberOfCompetitorsInLeaderboardFetcher { - private int numberOfCompetitors = -1; - - @Override - public int getNumberOfCompetitorsInLeaderboard() { - if (numberOfCompetitors == -1) { - numberOfCompetitors = Util.size(getCompetitors()); - } - return numberOfCompetitors; - } - } - - /** - * Handles the invalidation of the {@link SailingServiceImpl#raceDetailsAtEndOfTrackingCache} entries if the tracked - * race changes in any way. In particular, for {@link #statusChanged}, when the status changes away from LOADING, - * calculations may start or resume, making it necessary to clear the cache. - * - * @author Axel Uhl (D043530) - * - */ - private class CacheInvalidationListener extends AbstractRaceChangeListener { - private final TrackedRace trackedRace; - private final Competitor competitor; - - public CacheInvalidationListener(TrackedRace trackedRace, Competitor competitor) { - this.trackedRace = trackedRace; - this.competitor = competitor; - } - - public TrackedRace getTrackedRace() { - return trackedRace; - } - - public void removeFromTrackedRace() { - trackedRace.removeListener(this); - } - - private void invalidateCacheAndRemoveThisListenerFromTrackedRace() { - synchronized (raceDetailsAtEndOfTrackingCache) { - raceDetailsAtEndOfTrackingCache.remove(new com.sap.sse.common.Util.Pair(trackedRace, competitor)); - removeFromTrackedRace(); - } - } - - @Override - protected void defaultAction() { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - } - - private static class UUIDGenerator implements LeaderboardDTO.UUIDGenerator { - @Override - public String generateRandomUUID() { - return UUID.randomUUID().toString(); - } - } - - public AbstractSimpleLeaderboardImpl(ThresholdBasedResultDiscardingRule resultDiscardingRule) { - this.carriedPoints = new HashMap(); - this.scoreCorrection = createScoreCorrection(); - this.displayNames = new HashMap(); - this.crossLeaderboardResultDiscardingRule = resultDiscardingRule; - this.suppressedCompetitors = new HashSet(); - this.suppressedCompetitorsLock = new NamedReentrantReadWriteLock("suppressedCompetitorsLock", /* fair */ false); - this.raceColumnListeners = new RaceColumnListeners(); - this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); - initTransientFields(); - } - - /** - * Produces the score correction object to use in this leaderboard. Used by the constructor. Subclasses may override - * this method to create a more specific type of score correction. This implementation produces an object of type - * {@link ScoreCorrectionImpl}. - */ - protected SettableScoreCorrection createScoreCorrection() { - return new ScoreCorrectionImpl(this); - } - - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - ois.defaultReadObject(); - initTransientFields(); - } - - private void initTransientFields() { - this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); - this.raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); - this.cacheInvalidationListeners = new HashSet(); - // When many updates are triggered in a short period of time by a single thread, ensure that the single thread - // providing the updates is not outperformed by all the re-calculations happening here. Leave at least one - // core to other things, but by using at least three threads ensure that no simplistic deadlocks may occur. - final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3); - executor = new ThreadPoolExecutor(/* corePoolSize */ THREAD_POOL_SIZE, - /* maximumPoolSize */ THREAD_POOL_SIZE, - /* keepAliveTime */ 60, TimeUnit.SECONDS, - /* workQueue */ new LinkedBlockingQueue(), new ThreadFactoryWithPriority(Thread.NORM_PRIORITY-1, /* daemon */ true)); - } - - @Override - public void destroy() { - for (CacheInvalidationListener cacheInvalidationListener : cacheInvalidationListeners) { - cacheInvalidationListener.removeFromTrackedRace(); - } - } - - @Override - public SettableScoreCorrection getScoreCorrection() { - return scoreCorrection; - } - - @Override - public String getDisplayName(Competitor competitor) { - return displayNames.get(competitor); - } - - @Override - public String getDisplayName() { - return displayName; - } - - @Override - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - @Override - public ResultDiscardingRule getResultDiscardingRule() { - return crossLeaderboardResultDiscardingRule; - } - - @Override - public void setCarriedPoints(Competitor competitor, double carriedPoints) { - Double oldCarriedPoints = this.carriedPoints.put(competitor, carriedPoints); - if (!Util.equalsWithNull(oldCarriedPoints, carriedPoints)) { - getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, carriedPoints); - } - } - - @Override - public double getCarriedPoints(Competitor competitor) { - Double result = carriedPoints.get(competitor); - return result == null ? 0 : result; - } - - @Override - public Map getCompetitorsForWhichThereAreCarriedPoints() { - return Collections.unmodifiableMap(carriedPoints); - } - - @Override - public void unsetCarriedPoints(Competitor competitor) { - Double oldCarriedPoints = carriedPoints.remove(competitor); - if (oldCarriedPoints != null) { - getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, null); - } - } - - @Override - public boolean hasCarriedPoints() { - return !carriedPoints.isEmpty(); - } - - @Override - public boolean hasCarriedPoints(Competitor competitor) { - return carriedPoints.containsKey(competitor); - } - - @Override - public void setDisplayName(Competitor competitor, String displayName) { - String oldDisplayName = displayNames.get(competitor); - displayNames.put(competitor, displayName); - if (!Util.equalsWithNull(oldDisplayName, displayName)) { - getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); - } - } - - @Override - public void setCrossLeaderboardResultDiscardingRule(ThresholdBasedResultDiscardingRule discardingRule) { - ResultDiscardingRule oldDiscardingRule = getResultDiscardingRule(); - this.crossLeaderboardResultDiscardingRule = discardingRule; - getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, discardingRule); - } - - @Override - public Double getNetPoints(final Competitor competitor, final RaceColumn raceColumn, final TimePoint timePoint) { - return getScoreCorrection().getCorrectedScore( - new Callable() { - public Integer call() throws NoWindException { - return getTrackedRank(competitor, raceColumn, timePoint); - } - }, competitor, - raceColumn, timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()).getCorrectedScore(); - } - - @Override - public MaxPointsReason getMaxPointsReason(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - return getScoreCorrection().getMaxPointsReason(competitor, raceColumn, timePoint); - } - - @Override - public boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - return isDiscarded(competitor, raceColumn, getRaceColumns(), timePoint); - } - - private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, - Iterable raceColumnsToConsider, TimePoint timePoint) { - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - return isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns); - } - - /** - * Same as {@link #isDiscarded(Competitor, RaceColumn, TimePoint)}, only that the set of discarded race columns can - * be specified which is useful when total points are to be computed for more than one column for the same - * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done - * once and not again for each column (which would lead to quadratic effort). - * - * @param discardedRaceColumns - * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. - * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) - * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. - */ - private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, - final Set discardedRaceColumns) { - return !raceColumn.isMedalRace() - && getMaxPointsReason(competitor, raceColumn, timePoint).isDiscardable() - && discardedRaceColumns.contains(raceColumn); - } - - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { - return getTotalPoints(competitor, raceColumn, getRaceColumns(), timePoint); - } - - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, - Iterable raceColumnsToConsider, TimePoint timePoint) throws NoWindException { - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - return getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); - } - - /** - * Same as {@link #getTotalPoints(Competitor, RaceColumn, Iterable, TimePoint)}, only that the set of discarded race columns can - * be specified which is useful when total points are to be computed for more than one column for the same - * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done - * once and not again for each column (which would lead to quadratic effort). - * - * @param discardedRaceColumns - * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. - * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) - * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. - */ - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, Set discardedRaceColumns) { - Double result; - if (isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns)) { - result = 0.0; - } else { - final Double netPoints = getNetPoints(competitor, raceColumn, timePoint); - if (netPoints == null) { - result = null; - } else { - result = raceColumn.getFactor() * netPoints; - } - } - return result; - } - - @Override - public Double getTotalPoints(Competitor competitor, TimePoint timePoint) { - return getTotalPoints(competitor, getRaceColumns(), timePoint); - } - - @Override - public Double getTotalPoints(Competitor competitor, final Iterable raceColumnsToConsider, - TimePoint timePoint) { - // when a column with isStartsWithZeroScore() is found, only reset score if the competitor scored in any race from there on - boolean needToResetScoreUponNextNonEmptyEntry = false; - double result = getCarriedPoints(competitor); - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - for (RaceColumn raceColumn : raceColumnsToConsider) { - if (raceColumn.isStartsWithZeroScore()) { - needToResetScoreUponNextNonEmptyEntry = true; - } - if (getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint)) { - final Double totalPoints = getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); - if (totalPoints != null) { - if (needToResetScoreUponNextNonEmptyEntry) { - result = 0; - needToResetScoreUponNextNonEmptyEntry = false; - } - result += totalPoints; - } - } - } - return result; - } - - /** - * All competitors with non-null net points are added to the result which is then sorted by net points in ascending - * order. The fleet, if ordered, is the primary ordering criterion, followed by the net points. - */ - @Override - public List getCompetitorsFromBestToWorst(final RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { - final Map> netPointsAndFleet = new HashMap>(); - for (Competitor competitor : getCompetitors()) { - Double netPoints = getNetPoints(competitor, raceColumn, timePoint); - if (netPoints != null) { - netPointsAndFleet.put(competitor, new com.sap.sse.common.Util.Pair(netPoints, raceColumn.getFleetOfCompetitor(competitor))); - } - } - List result = new ArrayList(netPointsAndFleet.keySet()); - Collections.sort(result, new Comparator() { - @Override - public int compare(Competitor o1, Competitor o2) { - int comparisonResult; - if (o1 == o2) { - comparisonResult = 0; - } else { - if (raceColumn.hasSplitFleets() && !raceColumn.hasSplitFleetContiguousScoring()) { - // only check fleets if there are more than one and the column is not to be contiguously scored even in case - // of split fleets - final Fleet o1Fleet = netPointsAndFleet.get(o1).getB(); - final Fleet o2Fleet = netPointsAndFleet.get(o2).getB(); - if (o1Fleet == null) { - if (o2Fleet == null) { - comparisonResult = 0; - } else { - comparisonResult = 1; // o1 ranks "worse" because it doesn't have a fleet set while o2 - // has - } - } else { - if (o2Fleet == null) { - comparisonResult = -1; // o1 ranks "better" because it has a fleet set while o2 hasn't - } else { - comparisonResult = o1Fleet.compareTo(o2Fleet); - } - } - } else { - // either there are no split fleets or the split isn't relevant for scoring as for ordered fleets - // the scoring runs contiguously from top to bottom - comparisonResult = 0; - } - if (comparisonResult == 0) { - comparisonResult = getScoringScheme().getScoreComparator(/* nullScoresAreBetter */ false).compare( - netPointsAndFleet.get(o1).getA(), netPointsAndFleet.get(o2).getA()); - } - } - return comparisonResult; - } - }); - return result; - } - - @Override - public List getCompetitorsFromBestToWorst(TimePoint timePoint) { - return getCompetitorsFromBestToWorst(getRaceColumns(), timePoint); - } - - private List getCompetitorsFromBestToWorst(Iterable raceColumnsToConsider, TimePoint timePoint) { - List result = new ArrayList(); - for (Competitor competitor : getCompetitors()) { - result.add(competitor); - } - Collections.sort(result, getTotalRankComparator(raceColumnsToConsider, timePoint)); - return result; - } - - @Override - public int getTotalRankOfCompetitor(Competitor competitor, TimePoint timePoint) throws NoWindException { - List competitorsFromBestToWorst = getCompetitorsFromBestToWorst(timePoint); - return competitorsFromBestToWorst.indexOf(competitor) + 1; - } - - protected Comparator getTotalRankComparator(Iterable raceColumnsToConsider, TimePoint timePoint) { - return new LeaderboardTotalRankComparator(this, timePoint, getScoringScheme(), /* nullScoresAreBetter */ false, raceColumnsToConsider); - } - - @Override - public RaceColumn getRaceColumnByName(String columnName) { - RaceColumn result = null; - for (RaceColumn r : getRaceColumns()) { - if (r.getName().equals(columnName)) { - result = r; - break; - } - } - return result; - } - - @Override - public Competitor getCompetitorByName(String competitorName) { - for (Competitor competitor : getAllCompetitors()) { - if (competitor.getName().equals(competitorName)) { - return competitor; - } - } - return null; - } - - @Override - public boolean countRaceForComparisonWithDiscardingThresholds(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - TrackedRace trackedRaceForCompetitorInColumn; - return getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint) && - (getScoreCorrection().isScoreCorrected(competitor, raceColumn, timePoint) || - ((trackedRaceForCompetitorInColumn=raceColumn.getTrackedRace(competitor)) != null && - trackedRaceForCompetitorInColumn.hasStarted(timePoint) && - trackedRaceForCompetitorInColumn.getRank(competitor, timePoint) != 0)); - } - - @Override - public void addRaceColumnListener(RaceColumnListener listener) { - getRaceColumnListeners().addRaceColumnListener(listener); - } - - @Override - public void removeRaceColumnListener(RaceColumnListener listener) { - getRaceColumnListeners().removeRaceColumnListener(listener); - } - - @Override - public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint) throws NoWindException { - final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - return getEntry(competitor, race, timePoint, discardedRaceColumns); - } - - @Override - public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint, - Set discardedRaceColumns) throws NoWindException { - Callable trackedRankProvider = new Callable() { - public Integer call() throws NoWindException { - return getTrackedRank(competitor, race, timePoint); - } - }; - final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, race, - timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); - boolean discarded = isDiscarded(competitor, race, timePoint, discardedRaceColumns); - final Double correctedScore = correctedResults.getCorrectedScore(); - return new EntryImpl(trackedRankProvider, correctedScore, new Callable() { - @Override - public Double call() { - return correctedResults.getUncorrectedScore(); - } - }, correctedResults.isCorrected(), discarded ? DOUBLE_0 : correctedScore == null ? null - : Double.valueOf(correctedScore * race.getFactor()), correctedResults.getMaxPointsReason(), discarded, - race.getFleetOfCompetitor(competitor)); - } - - @Override - public Map> getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(TimePoint timePoint) throws NoWindException { - Map> result = new LinkedHashMap<>(); - List raceColumnsToConsider = new ArrayList<>(); - for (RaceColumn raceColumn : getRaceColumns()) { - raceColumnsToConsider.add(raceColumn); - result.put(raceColumn, getCompetitorsFromBestToWorst(raceColumnsToConsider, timePoint)); - } - return result; - } - - @Override - public Map> getTotalPointsSumAfterRaceColumn(final TimePoint timePoint) - throws NoWindException { - final Map> result = new LinkedHashMap<>(); - List raceColumnsToConsider = new ArrayList<>(); - Map>> futures = new HashMap<>(); - for (final RaceColumn raceColumn : getRaceColumns()) { - raceColumnsToConsider.add(raceColumn); - final Iterable finalRaceColumnsToConsider = new ArrayList<>(raceColumnsToConsider); - futures.put(raceColumn, executor.submit(new Callable>() { - @Override - public Map call() { - Map totalPointsSumPerCompetitorInColumn = new HashMap<>(); - for (Competitor competitor : getCompetitors()) { - totalPointsSumPerCompetitorInColumn.put(competitor, getTotalPoints(competitor, finalRaceColumnsToConsider, timePoint)); - } - synchronized (result) { - return totalPointsSumPerCompetitorInColumn; - } - } - })); - } - for (RaceColumn raceColumn : getRaceColumns()) { - try { - result.put(raceColumn, futures.get(raceColumn).get()); - } catch (InterruptedException | ExecutionException e) { - if (e.getCause() instanceof NoWindError) { - throw ((NoWindError) e.getCause()).getCause(); - } else { - throw new RuntimeException(e); // no caught exceptions occur in the futures executed - } - } - } - return result; - } - - @Override - public Map, Entry> getContent(final TimePoint timePoint) throws NoWindException { - Map, Entry> result = new HashMap, Entry>(); - Map> discardedRaces = new HashMap>(); - for (final RaceColumn raceColumn : getRaceColumns()) { - for (final Competitor competitor : getCompetitors()) { - Callable trackedRankProvider = new Callable() { - @Override - public Integer call() throws Exception { - return getTrackedRank(competitor, raceColumn, timePoint); - } - }; - final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, raceColumn, - timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); - Set discardedRacesForCompetitor = discardedRaces.get(competitor); - if (discardedRacesForCompetitor == null) { - discardedRacesForCompetitor = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - discardedRaces.put(competitor, discardedRacesForCompetitor); - } - boolean discarded = discardedRacesForCompetitor.contains(raceColumn); - final Double correctedScore = correctedResults.getCorrectedScore(); - Entry entry = new EntryImpl(trackedRankProvider, correctedScore, - new Callable() { @Override public Double call() { return correctedResults.getUncorrectedScore(); } }, - correctedResults.isCorrected(), - discarded ? DOUBLE_0 : (correctedScore==null?null: - Double.valueOf((correctedScore * raceColumn.getFactor()))), correctedResults.getMaxPointsReason(), - discarded, raceColumn.getFleetOfCompetitor(competitor)); - result.put(new com.sap.sse.common.Util.Pair(competitor, raceColumn), entry); - } - } - return result; - } - - @Override - public void trackedRaceLinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); - } - - @Override - public void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); - // It's generally possible that a leaderboard links to the same tracked race in multiple columns / fleets; - // only if it no longer references the trackedRace currently unlinked from one column/fleet, also unlink - // all cache invalidation listeners for said trackedRace - if (!Util.contains(getTrackedRaces(), trackedRace)) { - synchronized (cacheInvalidationListeners) { - for (Iterator cacheInvalidationListenerIter=cacheInvalidationListeners.iterator(); - cacheInvalidationListenerIter.hasNext(); ) { - CacheInvalidationListener cacheInvalidationListener = cacheInvalidationListenerIter.next(); - if (cacheInvalidationListener.getTrackedRace() == trackedRace) { - cacheInvalidationListener.removeFromTrackedRace(); - cacheInvalidationListenerIter.remove(); - } - } - } - } - } - - @Override - public void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { - getRaceColumnListeners().notifyListenersAboutIsMedalRaceChanged(raceColumn, newIsMedalRace); - } - - @Override - public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { - getRaceColumnListeners().notifyListenersAboutIsStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); - } - - @Override - public void isFirstColumnIsNonDiscardableCarryForwardChanged(RaceColumn raceColumn, boolean firstColumnIsNonDiscardableCarryForward) { - getRaceColumnListeners().notifyListenersAboutIsFirstColumnIsNonDiscardableCarryForwardChanged(raceColumn, firstColumnIsNonDiscardableCarryForward); - } - - @Override - public void hasSplitFleetContiguousScoringChanged(RaceColumn raceColumn, boolean hasSplitFleetContiguousScoring) { - getRaceColumnListeners().notifyListenersAboutHasSplitFleetContiguousScoringChanged(raceColumn, hasSplitFleetContiguousScoring); - } - - @Override - public void raceColumnMoved(RaceColumn raceColumn, int newIndex) { - getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(raceColumn, newIndex); - } - - @Override - public void factorChanged(RaceColumn raceColumn, Double oldFactor, Double newFactor) { - getRaceColumnListeners().notifyListenersAboutFactorChanged(raceColumn, oldFactor, newFactor); - } - - /** - * A leaderboard will only accept the addition of a race column if the column's name is unique across the leaderboard. - */ - @Override - public boolean canAddRaceColumnToContainer(RaceColumn newRaceColumn) { - boolean result = true; - for (RaceColumn raceColumn : getRaceColumns()) { - if (raceColumn.getName().equals(newRaceColumn.getName())) { - result = false; - break; - } - } - return result; - } - - @Override - public void raceColumnAddedToContainer(RaceColumn raceColumn) { - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(raceColumn); - } - - @Override - public void raceColumnRemovedFromContainer(RaceColumn raceColumn) { - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); - } - - @Override - public void competitorDisplayNameChanged(Competitor competitor, String oldDisplayName, String displayName) { - getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); - } - - @Override - public void resultDiscardingRuleChanged(ResultDiscardingRule oldDiscardingRule, ResultDiscardingRule newDiscardingRule) { - getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, newDiscardingRule); - } - - @Override - public boolean isTransient() { - return false; - } - - /** - * Finds out the time point when any of the {@link Leaderboard#getTrackedRaces() tracked races currently attached to - * the leaderboard} and the {@link Leaderboard#getScoreCorrection() score corrections} have last been - * modified. If no tracked race is attached and no time-stamped score corrections have been applied to the leaderboard, - * null is returned. The time point computed this way is a good choice for normalizing queries for later time - * points in an attempt to achieve more cache hits.

    - * - * Note, however, that the result does not tell about structural changes to the leaderboard and therefore cannot be used - * to determine the need for cache invalidation. For example, if a column is added to a leaderboard after the time point - * returned by this method but that column's attached tracked race has finished before the time point returned by this method, - * the result of this method won't change. Still, the contents of the leaderboard will change by a change in column structure. - * A different means to determine the possibility of changes that happened to this leaderboard must be used for cache - * management. Such a facility has to listen for score correction changes, tracked races being attached or detached and - * the column structure changing. - * - * @see TrackedRace#getTimePointOfNewestEvent() - * @see SettableScoreCorrection#getTimePointOfLastCorrectionsValidity() - */ - @Override - public TimePoint getTimePointOfLatestModification() { - TimePoint result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - if (result == null || (trackedRace.getTimePointOfNewestEvent() != null && trackedRace.getTimePointOfNewestEvent().after(result))) { - result = trackedRace.getTimePointOfNewestEvent(); - } - } - TimePoint timePointOfLastScoreCorrection = getScoreCorrection().getTimePointOfLastCorrectionsValidity(); - if (timePointOfLastScoreCorrection != null && (result == null || timePointOfLastScoreCorrection.after(result))) { - result = timePointOfLastScoreCorrection; - } - return result; - } - - @Override - public com.sap.sse.common.Util.Pair getMaximumSpeedOverGround(Competitor competitor, TimePoint timePoint) { - com.sap.sse.common.Util.Pair result = null; - // TODO should we ensure that competitor participated in all race columns? - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = markPassings.first().getTimePoint(); - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) && - markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - to = timePoint; - } - com.sap.sse.common.Util.Pair maxSpeed = trackedRace.getTrack(competitor).getMaximumSpeedOverGround(from, to); - if (result == null || result.getB() == null || - (maxSpeed != null && maxSpeed.getB() != null && maxSpeed.getB().compareTo(result.getB()) > 0)) { - result = maxSpeed; - } - } - } - } - return result; - } - - @Override - public Speed getAverageSpeedOverGround(Competitor competitor, TimePoint timePoint) { - Speed result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = markPassings.first().getTimePoint(); - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) && - markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - if (markPassings.last().getWaypoint() != trackedRace.getRace().getCourse().getLastWaypoint() && - timePoint.after(markPassings.last().getTimePoint())) { - result = null; - break; - } - to = timePoint; - } - Distance distanceTraveled = trackedRace.getDistanceTraveled(competitor, timePoint); - if (distanceTraveled != null) { - result = distanceTraveled.inTime(to.asMillis()-from.asMillis()); - } - } - } - } - return result; - } - - @Override - public Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint) throws NoWindException { - return getTotalTimeSailedInLegType(competitor, legType, timePoint, new HashMap()); - } - - private Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint, Map legTypeCache) throws NoWindException { - Duration result = null; - // TODO should we ensure that competitor participated in all race columns? - outerLoop: - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - trackedRace.getRace().getCourse().lockForRead(); - try { - for (Leg leg : trackedRace.getRace().getCourse().getLegs()) { - TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, leg); - if (trackedLegOfCompetitor.hasStartedLeg(timePoint)) { - // find out leg type at the time the competitor started the leg - try { - final TrackedLeg trackedLeg = trackedRace.getTrackedLeg(leg); - LegType trackedLegType = legTypeCache.get(trackedLeg); - if (trackedLegType == null) { - final TimePoint startTime = trackedLegOfCompetitor.getStartTime(); - TimePoint finishTime = trackedLegOfCompetitor.getFinishTime(); - if (finishTime == null) { - finishTime = timePoint; - } - trackedLegType = trackedLeg.getLegType(startTime.plus(startTime.until(finishTime).divide(2))); // middle of the leg - legTypeCache.put(trackedLeg, trackedLegType); - } - if (legType == trackedLegType) { - Duration timeSpentOnDownwind = trackedLegOfCompetitor.getTime(timePoint); - if (timeSpentOnDownwind != null) { - if (result == null) { - result = timeSpentOnDownwind; - } else { - result = result.plus(timeSpentOnDownwind); - } - } else { - // Although the competitor has started the leg, no value was produced. This - // means that the competitor didn't finish the leg before tracking ended. No useful value - // can be obtained for this competitor anymore. - result = null; - break outerLoop; - } - } - } catch (NoWindException nwe) { - // without wind there is no leg type and hence there is no reasonable value for this: - result = null; - break outerLoop; - } - } - } - } finally { - trackedRace.getRace().getCourse().unlockAfterRead(); - } - } - } - return result; - } - - @Override - public Duration getTotalTimeSailed(Competitor competitor, TimePoint timePoint) { - Duration result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = trackedRace.getStartOfRace(); // start counting at race start, not when the competitor passed the line - if (from != null && !timePoint.before(from)) { // but only if the race started after timePoint - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) - && markPassings.last().getWaypoint() == trackedRace.getRace().getCourse() - .getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - if (trackedRace.getEndOfTracking() != null - && timePoint.after(trackedRace.getEndOfTracking())) { - result = null; // race not finished until end of tracking; no reasonable value can be - // computed for competitor - break; - } else { - to = timePoint; - } - } - Duration timeSpent = from.until(to); - if (result == null) { - result = timeSpent; - } else { - result=result.plus(timeSpent); - } - } - } - } - } - return result; - } - - @Override - public Distance getTotalDistanceTraveled(Competitor competitor, TimePoint timePoint) { - Distance result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - TimePoint startOfRace; - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor) && - (startOfRace=trackedRace.getStartOfRace()) != null && - !startOfRace.after(timePoint)) { - Distance distanceSailedInRace = trackedRace.getDistanceTraveled(competitor, timePoint); - if (distanceSailedInRace != null) { - if (result == null) { - result = distanceSailedInRace; - } else { - result = result.add(distanceSailedInRace); - } - } else { - // if competitor has not finished one single race in the whole - // series then we can not return a meaningful value for all - // all races - return null; - } - } - } - return result; - } - - protected RaceColumnListeners getRaceColumnListeners() { - return raceColumnListeners; - } - - @Override - public Iterable getCompetitors() { - final Iterable result; - // mostly the set of suppressed competitors is empty; in this case, avoid having to loop over the - // potentially large set of competitors - if (Util.isEmpty(getSuppressedCompetitors())) { - result = getAllCompetitors(); - } else { - final Iterable allCompetitors = getAllCompetitors(); - final Set suppressed = new HashSet<>(); - Util.addAll(getSuppressedCompetitors(), suppressed); - result = getCompetitorIterableSkippingSuppressed(allCompetitors, suppressed); - } - return result; - } - - @Override - public Iterable getCompetitors(RaceColumn raceColumn, Fleet fleet) { - return getCompetitorIterableSkippingSuppressed(getAllCompetitors(raceColumn, fleet), getSuppressedCompetitors()); - } - - /** - * return an iterable with a smart iterator that filters out the suppressed elements on demand - */ - protected Iterable getCompetitorIterableSkippingSuppressed(final Iterable allCompetitors, - final Iterable suppressed) { - final Iterable result; - result = new Iterable() { - @Override - public Iterator iterator() { - return new Iterator() { - private final Iterator allIter = allCompetitors.iterator(); - private Competitor next = advance(); - - private Competitor advance() { - next = null; - while (allIter.hasNext() && next == null) { - next = allIter.next(); - if (Util.contains(suppressed, next)) { - next = null; - } - } - return next; - } - - @Override - public boolean hasNext() { - return next != null; - } - - @Override - public Competitor next() { - if (next == null) { - throw new NoSuchElementException(); - } - final Competitor result = next; - advance(); - return result; - } - }; - } - }; - return result; - } - - @Override - public Iterable getSuppressedCompetitors() { - LockUtil.lockForRead(suppressedCompetitorsLock); - try { - return new HashSet(suppressedCompetitors); - } finally { - LockUtil.unlockAfterRead(suppressedCompetitorsLock); - } - } - - @Override - public void setSuppressed(Competitor competitor, boolean suppressed) { - LockUtil.lockForWrite(suppressedCompetitorsLock); - try { - if (suppressed) { - suppressedCompetitors.add(competitor); - } else { - suppressedCompetitors.remove(competitor); - } - } finally { - LockUtil.unlockAfterWrite(suppressedCompetitorsLock); - } - getScoreCorrection().notifyListenersAboutIsSuppressedChange(competitor, suppressed); - } - - @Override - public TimePoint getNowMinusDelay() { - final TimePoint now = MillisecondsTimePoint.now(); - final Long delayToLiveInMillis = getDelayToLiveInMillis(); - TimePoint timePoint = delayToLiveInMillis == null ? now : now.minus(delayToLiveInMillis); - return timePoint; - } - - @Override - public void raceLogEventAdded(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { - getRaceColumnListeners().notifyListenersAboutRaceLogEventAdded(raceColumn, raceLogIdentifier, event); - if (event instanceof InvalidatesLeaderboardCache) { - // make sure to invalidate the cache as this event indicates that - // it changes values the cache could still hold - if (leaderboardDTOCache != null) { - leaderboardDTOCache.invalidate(this); - } - } - } - - @Override - public LeaderboardDTO computeDTO(final TimePoint timePoint, - final Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, - final boolean waitForLatestAnalyses, TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory, - final boolean fillNetPointsUncorrected) - throws NoWindException { - long startOfRequestHandling = System.currentTimeMillis(); - final LeaderboardDTOCalculationReuseCache cache = new LeaderboardDTOCalculationReuseCache(timePoint); - final LeaderboardDTO result = new LeaderboardDTO(this.getScoreCorrection().getTimePointOfLastCorrectionsValidity() == null ? null - : this.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(), - this.getScoreCorrection() == null ? null : this.getScoreCorrection().getComment(), - this.getScoringScheme() == null ? null : this.getScoringScheme().getType(), this - .getScoringScheme().isHigherBetter(), new UUIDGenerator(), addOverallDetails); - result.competitors = new ArrayList(); - result.name = this.getName(); - result.competitorDisplayNames = new HashMap(); - for (Competitor suppressedCompetitor : this.getSuppressedCompetitors()) { - result.setSuppressed(baseDomainFactory.convertToCompetitorDTO(suppressedCompetitor), true); - } - // Now create the race columns and, as a future task, set their competitorsFromBestToWorst, then wait for all these - // futures to finish: - Map>> competitorsFromBestToWorstTasks = new HashMap<>(); - for (final RaceColumn raceColumn : this.getRaceColumns()) { - result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(), - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null); - for (Fleet fleet : raceColumn.getFleets()) { - RegattaAndRaceIdentifier raceIdentifier = null; - RaceDTO race = null; - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - final FleetDTO fleetDTO = baseDomainFactory.convertToFleetDTO(fleet); - if (trackedRace != null) { - raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta().getName(), - trackedRace.getRace().getName()); - race = baseDomainFactory.createRaceDTO(trackedRegattaRegistry, /* withGeoLocationData */ false, raceIdentifier, trackedRace); - } - // Note: the RaceColumnDTO won't be created by the following addRace call because it has been created - // above by the result.createEmptyRaceColumn call - result.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, - fleetDTO, raceColumn.isMedalRace(), raceIdentifier, race); - } - FutureTask> task = new FutureTask>( - () -> baseDomainFactory.getCompetitorDTOList(AbstractSimpleLeaderboardImpl.this.getCompetitorsFromBestToWorst(raceColumn, timePoint))); - executor.execute(task); - competitorsFromBestToWorstTasks.put(raceColumn, task); - } - // wait for the competitor orderings to have been computed for all race columns before continuing; subsequent tasks may depend on these data - for (Map.Entry>> raceColumnAndTaskToJoin : competitorsFromBestToWorstTasks.entrySet()) { - try { - result.setCompetitorsFromBestToWorst(raceColumnAndTaskToJoin.getKey().getName(), raceColumnAndTaskToJoin.getValue().get()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept - // null values. - // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So - // this helps robustness from a user's perspective. - logger.log( - Level.SEVERE, - AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " - + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails - + "): exception during computing competitor ordering for race column "+raceColumnAndTaskToJoin.getKey().getName(), e); - } - } - result.setDelayToLiveInMillisForLatestRace(this.getDelayToLiveInMillis()); - result.rows = new HashMap(); - result.hasCarriedPoints = this.hasCarriedPoints(); - if (this.getResultDiscardingRule() instanceof ThresholdBasedResultDiscardingRule) { - result.discardThresholds = ((ThresholdBasedResultDiscardingRule) this.getResultDiscardingRule()) - .getDiscardIndexResultsStartingWithHowManyRaces(); - } else { - result.discardThresholds = null; - } - // Computing the competitor leg ranks is expensive, especially in live mode, in case new events keep - // invalidating the ranks cache in TrackedLegImpl. The problem then is that the sorting based on wind data is repeated for - // each competitor, leading to square effort. We therefore need to compute the leg ranks for those races where leg - // details are requested only once and pass them into getLeaderboardEntryDTO - final Map> legRanksCache = new HashMap>(); - for (final RaceColumn raceColumn : this.getRaceColumns()) { - // if details for the column are requested, cache the leg's ranks - if (namesOfRaceColumnsForWhichToLoadLegDetails != null - && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName())) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - trackedRace.getRace().getCourse().lockForRead(); - try { - for (TrackedLeg trackedLeg : trackedRace.getTrackedLegs()) { - legRanksCache.put(trackedLeg.getLeg(), trackedLeg.getRanks(timePoint, cache)); - } - } finally { - trackedRace.getRace().getCourse().unlockAfterRead(); - } - } - } - } - } - for (final Competitor competitor : this.getCompetitorsFromBestToWorst(timePoint)) { - CompetitorDTO competitorDTO = baseDomainFactory.convertToCompetitorDTO(competitor); - LeaderboardRowDTO row = new LeaderboardRowDTO(); - row.competitor = competitorDTO; - row.fieldsByRaceColumnName = new HashMap(); - row.carriedPoints = this.hasCarriedPoints(competitor) ? this.getCarriedPoints(competitor) : null; - row.totalPoints = this.getTotalPoints(competitor, timePoint); - if (addOverallDetails) { - addOverallDetailsToRow(timePoint, competitor, row); - } - result.competitors.add(competitorDTO); - Map> futuresForColumnName = new HashMap>(); - final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - for (final RaceColumn raceColumn : this.getRaceColumns()) { - RunnableFuture future = new FutureTask(() -> { - Entry entry = AbstractSimpleLeaderboardImpl.this.getEntry(competitor, raceColumn, timePoint, discardedRaceColumns); - return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails != null - && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn - .getName()), waitForLatestAnalyses, legRanksCache, baseDomainFactory, - fillNetPointsUncorrected, cache); - }); - executor.execute(future); - futuresForColumnName.put(raceColumn.getName(), future); - } - for (Map.Entry> raceColumnNameAndFuture : futuresForColumnName.entrySet()) { - try { - row.fieldsByRaceColumnName.put(raceColumnNameAndFuture.getKey(), raceColumnNameAndFuture.getValue().get()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept - // null values. - // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So - // this helps robustness from a user's perspective. - logger.log( - Level.SEVERE, - AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " - + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails - + "): exception during computing leaderboard entry for competitor " - + competitor.getName() + " in race column " + raceColumnNameAndFuture.getKey() - + ". Leaving empty.", e); - } - } - result.rows.put(competitorDTO, row); - String displayName = this.getDisplayName(competitor); - if (displayName != null) { - result.competitorDisplayNames.put(competitorDTO, displayName); - } - } - logger.info("computeLeaderboardByName(" + this.getName() + ", " + timePoint + ", " - + namesOfRaceColumnsForWhichToLoadLegDetails + ", addOverallDetails=" + addOverallDetails + ") took " - + (System.currentTimeMillis() - startOfRequestHandling) + "ms"); - return result; - } - - private void addOverallDetailsToRow(final TimePoint timePoint, - final Competitor competitor, LeaderboardRowDTO row) throws NoWindException { - final com.sap.sse.common.Util.Pair maximumSpeedOverGround = this.getMaximumSpeedOverGround(competitor, timePoint); - if (maximumSpeedOverGround != null && maximumSpeedOverGround.getB() != null) { - row.maximumSpeedOverGroundInKnots = maximumSpeedOverGround.getB().getKnots(); - row.whenMaximumSpeedOverGroundWasAchieved = maximumSpeedOverGround.getA().getTimePoint().asDate(); - } - Map legTypeCache = new HashMap<>(); - final Duration totalTimeSailedDownwind = this.getTotalTimeSailedInLegType(competitor, LegType.DOWNWIND, timePoint, legTypeCache); - row.totalTimeSailedDownwindInSeconds = totalTimeSailedDownwind==null?null:totalTimeSailedDownwind.asSeconds(); - final Duration totalTimeSailedUpwind = this.getTotalTimeSailedInLegType(competitor, LegType.UPWIND, timePoint, legTypeCache); - row.totalTimeSailedUpwindInSeconds = totalTimeSailedUpwind==null?null:totalTimeSailedUpwind.asSeconds(); - final Duration totalTimeSailedReaching = this.getTotalTimeSailedInLegType(competitor, LegType.REACHING, timePoint, legTypeCache); - row.totalTimeSailedReachingInSeconds = totalTimeSailedReaching==null?null:totalTimeSailedReaching.asSeconds(); - final Duration totalTimeSailed = this.getTotalTimeSailed(competitor, timePoint); - row.totalTimeSailedInSeconds = totalTimeSailed==null?null:totalTimeSailed.asSeconds(); - final Distance totalDistanceTraveledInMeters = this.getTotalDistanceTraveled(competitor, timePoint); - row.totalDistanceTraveledInMeters = totalDistanceTraveledInMeters==null?null:totalDistanceTraveledInMeters.getMeters(); - } - - /** - * @param waitForLatestAnalyses - * if false, this method is allowed to read the maneuver analysis results from a cache that - * may not reflect all data already received; otherwise, the method will always block for the latest - * cache updates to have happened before returning. - * @param fillNetPointsUncorrected - * tells if {@link LeaderboardEntryDTO#netPointsUncorrected} shall be filled; filling it is rather - * expensive, especially when compared to simply retrieving a score correction, and particularly if in a - * larger fleet a number of competitors haven't properly finished the race. This should only be used for - * leaderboard editing where a user needs to see what the uncorrected score was that would be used when - * the correction was removed. - */ - private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, RaceColumn raceColumn, Competitor competitor, - TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses, - Map> legRanksCache, DomainFactory baseDomainFactory, - boolean fillNetPointsUncorrected, WindLegTypeAndLegBearingCache cache) { - LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); - TrackedRace trackedRace = raceColumn.getTrackedRace(competitor); - entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); - entryDTO.netPoints = entry.getNetPoints(); - if (fillNetPointsUncorrected) { - entryDTO.netPointsUncorrected = entry.getNetPointsUncorrected(); - } - entryDTO.netPointsCorrected = entry.isNetPointsCorrected(); - entryDTO.totalPoints = entry.getTotalPoints(); - entryDTO.reasonForMaxPoints = entry.getMaxPointsReason(); - entryDTO.discarded = entry.isDiscarded(); - final GPSFixTrack track = trackedRace == null ? null : trackedRace.getTrack(competitor); - if (trackedRace != null) { - Date timePointOfLastPositionFixAtOrBeforeQueryTimePoint = getTimePointOfLastFixAtOrBefore(competitor, trackedRace, timePoint); - if (track != null) { - entryDTO.averageSamplingInterval = track.getAverageIntervalBetweenRawFixes(); - } - if (timePointOfLastPositionFixAtOrBeforeQueryTimePoint != null) { - long timeDifferenceInMs = timePoint.asMillis() - timePointOfLastPositionFixAtOrBeforeQueryTimePoint.getTime(); - entryDTO.timeSinceLastPositionFixInSeconds = timeDifferenceInMs == 0 ? 0.0 : timeDifferenceInMs / 1000.0; - } else { - entryDTO.timeSinceLastPositionFixInSeconds = null; - } - } - if (addLegDetails && trackedRace != null) { - try { - final RankingInfo rankingInfo = trackedRace.getRankingMetric().getRankingInfo(timePoint, cache); - RaceDetails raceDetails = getRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, - legRanksCache, rankingInfo, cache); - entryDTO.legDetails = raceDetails.getLegDetails(); - entryDTO.windwardDistanceToCompetitorFarthestAheadInMeters = raceDetails.getWindwardDistanceToCompetitorFarthestAhead() == null ? null - : raceDetails.getWindwardDistanceToCompetitorFarthestAhead().getMeters(); - entryDTO.gapToLeaderInOwnTime = trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache); - entryDTO.averageAbsoluteCrossTrackErrorInMeters = raceDetails.getAverageAbsoluteCrossTrackError() == null ? null - : raceDetails.getAverageAbsoluteCrossTrackError().getMeters(); - entryDTO.averageSignedCrossTrackErrorInMeters = raceDetails.getAverageSignedCrossTrackError() == null ? null - : raceDetails.getAverageSignedCrossTrackError().getMeters(); - entryDTO.calculatedTime = raceDetails.getCorrectedTime(); - entryDTO.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = raceDetails.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead(); - entryDTO.gapToLeaderInOwnTime = raceDetails.getGapToLeaderInOwnTime(); - final TimePoint startOfRace = trackedRace.getStartOfRace(); - if (startOfRace != null) { - Waypoint startWaypoint = trackedRace.getRace().getCourse().getFirstWaypoint(); - NavigableSet competitorMarkPassings = trackedRace.getMarkPassings(competitor); - trackedRace.lockForRead(competitorMarkPassings); - try { - if (!competitorMarkPassings.isEmpty()) { - final MarkPassing firstMarkPassing = competitorMarkPassings.iterator().next(); - if (firstMarkPassing.getWaypoint() == startWaypoint) { - Distance distanceToStartLineFiveSecondsBeforeStartOfRace = trackedRace.getDistanceToStartLine(competitor, /*milliseconds before start*/ 5000); - entryDTO.distanceToStartLineFiveSecondsBeforeStartInMeters = distanceToStartLineFiveSecondsBeforeStartOfRace == null ? null - : distanceToStartLineFiveSecondsBeforeStartOfRace.getMeters(); - Speed speedFiveSecondsBeforeStartOfRace = trackedRace.getSpeed(competitor, /*milliseconds before start*/ 5000); - entryDTO.speedOverGroundFiveSecondsBeforeStartInKnots = speedFiveSecondsBeforeStartOfRace == null ? null - : speedFiveSecondsBeforeStartOfRace.getKnots(); - Distance distanceToStartLineAtStartOfRace = trackedRace.getDistanceToStartLine( - competitor, startOfRace); - entryDTO.distanceToStartLineAtStartOfRaceInMeters = distanceToStartLineAtStartOfRace == null ? null - : distanceToStartLineAtStartOfRace.getMeters(); - Speed speedAtStartTime = track == null ? null : track.getEstimatedSpeed(startOfRace); - entryDTO.speedOverGroundAtStartOfRaceInKnots = speedAtStartTime == null ? null - : speedAtStartTime.getKnots(); - TimePoint competitorStartTime = firstMarkPassing.getTimePoint(); - entryDTO.timeBetweenRaceStartAndCompetitorStartInSeconds = startOfRace.until(competitorStartTime).asSeconds(); - Speed competitorSpeedWhenPassingStart = track == null ? null : track - .getEstimatedSpeed(competitorStartTime); - entryDTO.speedOverGroundAtPassingStartWaypointInKnots = competitorSpeedWhenPassingStart == null ? null - : competitorSpeedWhenPassingStart.getKnots(); - try { - entryDTO.startTack = trackedRace.getTack(competitor, competitorStartTime); - } catch (NoWindException nwe) { - entryDTO.startTack = null; // leave empty in case no wind information is available - } - Distance distanceFromStarboardSideOfStartLineWhenPassingStart = trackedRace - .getDistanceFromStarboardSideOfStartLineWhenPassingStart(competitor); - entryDTO.distanceToStarboardSideOfStartLineInMeters = distanceFromStarboardSideOfStartLineWhenPassingStart == null ? null - : distanceFromStarboardSideOfStartLineWhenPassingStart.getMeters(); - } - } - } finally { - trackedRace.unlockAfterRead(competitorMarkPassings); - } - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - throw new RuntimeException(e); // the future used to calculate the leg details was interrupted; escalate as runtime exception - } - } - final Fleet fleet = entry.getFleet(); - entryDTO.fleet = fleet == null ? null : baseDomainFactory.convertToFleetDTO(fleet); - return entryDTO; - } - - /** - * Determines the time point of the last raw fix (with outliers not removed) for competitor in - * trackedRace. If the competitor's track is null or empty, null is returned. - * @param trackedRace must not be null - * @param atOrBefore find the last fix at or before the time point specified - */ - private Date getTimePointOfLastFixAtOrBefore(Competitor competitor, TrackedRace trackedRace, TimePoint atOrBefore) { - assert trackedRace != null; - final Date timePointOfLastPositionFix; - GPSFixTrack track = trackedRace.getTrack(competitor); - if (track == null) { - timePointOfLastPositionFix = null; - } else { - GPSFixMoving lastFix = track.getLastFixAtOrBefore(atOrBefore); - if (lastFix == null) { - timePointOfLastPositionFix = null; - } else { - timePointOfLastPositionFix = lastFix.getTimePoint().asDate(); - } - } - return timePointOfLastPositionFix; - } - - private static class RaceDetails { - private final List legDetails; - private final Distance windwardDistanceToCompetitorFarthestAhead; - private final Distance averageAbsoluteCrossTrackError; - private final Distance averageSignedCrossTrackError; - private final Duration gapToLeaderInOwnTime; - private final Duration correctedTime; - private final Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - - public RaceDetails(List legDetails, Distance windwardDistanceToCompetitorFarthestAhead, - Distance averageAbsoluteCrossTrackError, Distance averageSignedCrossTrackError, - Duration gapToLeaderInOwnTime, Duration correctedTime, - Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { - super(); - this.legDetails = legDetails; - this.windwardDistanceToCompetitorFarthestAhead = windwardDistanceToCompetitorFarthestAhead; - this.averageAbsoluteCrossTrackError = averageAbsoluteCrossTrackError; - this.averageSignedCrossTrackError = averageSignedCrossTrackError; - this.gapToLeaderInOwnTime = gapToLeaderInOwnTime; - this.correctedTime = correctedTime; - this.correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - } - public List getLegDetails() { - return legDetails; - } - public Distance getWindwardDistanceToCompetitorFarthestAhead() { - return windwardDistanceToCompetitorFarthestAhead; - } - public Distance getAverageAbsoluteCrossTrackError() { - return averageAbsoluteCrossTrackError; - } - public Distance getAverageSignedCrossTrackError() { - return averageSignedCrossTrackError; - } - public Duration getGapToLeaderInOwnTime() { - return gapToLeaderInOwnTime; - } - public Duration getCorrectedTime() { - return correctedTime; - } - public Duration getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead() { - return correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - } - } - - /** - * If timePoint is after the end of the race's tracking the query will be adjusted to obtain the values - * at the end of the {@link TrackedRace#getEndOfTracking() race's tracking time}. If the time point adjusted this - * way equals the end of the tracking time, the query results will be looked up in a cache first and if not found, - * they will be stored to the cache after calculating them. A cache invalidation {@link RaceChangeListener listener} - * will be registered with the race which will be triggered for any event received by the race. - * @param waitForLatestAnalyses - * if false, this method is allowed to read the maneuver analysis results from a cache that - * may not reflect all data already received; otherwise, the method will always block for the latest - * cache updates to have happened before returning. - */ - private RaceDetails getRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { - final RaceDetails raceDetails; - if (trackedRace.getEndOfTracking() != null && trackedRace.getEndOfTracking().compareTo(timePoint) < 0) { - raceDetails = getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(trackedRace, competitor, legRanksCache, rankingInfo, cache); - } else { - raceDetails = calculateRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache, cache, rankingInfo); - } - return raceDetails; - } - - private RaceDetails getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(final TrackedRace trackedRace, - final Competitor competitor, final Map> legRanksCache, - RankingInfo rankingInfo, final WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { - final com.sap.sse.common.Util.Pair key = new com.sap.sse.common.Util.Pair(trackedRace, competitor); - RunnableFuture raceDetails; - synchronized (raceDetailsAtEndOfTrackingCache) { - raceDetails = raceDetailsAtEndOfTrackingCache.get(key); - if (raceDetails == null) { - raceDetails = new FutureTask(new Callable() { - @Override - public RaceDetails call() throws Exception { - TimePoint end = trackedRace.getEndOfRace(); - if (end == null) { - end = trackedRace.getEndOfTracking(); - } - return calculateRaceDetails(trackedRace, competitor, end, - // TODO see bug 1358: for now, use waitForLatest==false until we've switched to optimistic locking for the course read lock - /* TODO old comment when it was still true: "because this is done only once after end of tracking" */ - /* waitForLatestAnalyses (maneuver and cross track error) */ false, - legRanksCache, cache, rankingInfo); - } - }); - raceDetailsExecutor.execute(raceDetails); - raceDetailsAtEndOfTrackingCache.put(key, raceDetails); - final CacheInvalidationListener cacheInvalidationListener = new CacheInvalidationListener(trackedRace, competitor); - trackedRace.addListener(cacheInvalidationListener); - cacheInvalidationListeners.add(cacheInvalidationListener); - } - } - return raceDetails.get(); - } - - private RaceDetails calculateRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - WindLegTypeAndLegBearingCache cache, final RankingInfo rankingInfo) { - final List legDetails = new ArrayList(); - final Course course = trackedRace.getRace().getCourse(); - course.lockForRead(); // hold back any course re-configurations while looping over the legs - try { - for (Leg leg : course.getLegs()) { - LegEntryDTO legEntry; - // We loop over a copy of the course's legs; during a course change, legs may become "stale," even with - // regard to the leg/trackedLeg structures inside the tracked race which is updated by the course change - // immediately. That's why we've acquired a read lock for the course above. - TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, leg); - if (trackedLeg != null && trackedLeg.hasStartedLeg(timePoint)) { - legEntry = createLegEntry(trackedLeg, timePoint, waitForLatestAnalyses, legRanksCache, rankingInfo, cache); - } else { - legEntry = null; - } - legDetails.add(legEntry); - } - final Distance windwardDistanceToCompetitorFarthestAhead = trackedRace == null ? null : trackedRace - .getWindwardDistanceToCompetitorFarthestAhead(competitor, timePoint, WindPositionMode.LEG_MIDDLE, rankingInfo, cache); - Distance averageAbsoluteCrossTrackError; - try { - averageAbsoluteCrossTrackError = trackedRace == null ? null : trackedRace.getAverageAbsoluteCrossTrackError( - competitor, timePoint, waitForLatestAnalyses, cache); - } catch (NoWindException nwe) { - // without wind information, use null meaning "unknown" - averageAbsoluteCrossTrackError = null; - } - Distance averageSignedCrossTrackError; - try { - averageSignedCrossTrackError = trackedRace == null ? null : trackedRace.getAverageSignedCrossTrackError( - competitor, timePoint, waitForLatestAnalyses, cache); - } catch (NoWindException nwe) { - // without wind information, use null meaning "unknown" - averageSignedCrossTrackError = null; - } - final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); - return new RaceDetails(legDetails, windwardDistanceToCompetitorFarthestAhead, averageAbsoluteCrossTrackError, averageSignedCrossTrackError, - trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache), - trackedRace.getRankingMetric().getCorrectedTime(competitor, timePoint), - competitorRankingInfo == null ? null : competitorRankingInfo.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead()); - } finally { - course.unlockAfterRead(); - } - } - - private LegEntryDTO createLegEntry(TrackedLegOfCompetitor trackedLeg, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { - LegEntryDTO result; - final Duration time = trackedLeg.getTime(timePoint); - if (trackedLeg == null || time == null) { - result = null; - } else { - result = new LegEntryDTO(); - try { - result.legType = trackedLeg.getTrackedLeg().getLegType(timePoint); - } catch (NoWindException nwe) { - result.legType = null; // can't determine leg type without wind data - } - final Speed averageSpeedOverGround = trackedLeg.getAverageSpeedOverGround(timePoint); - result.averageSpeedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); - Distance averageAbsoluteCrossTrackError; - try { - averageAbsoluteCrossTrackError = trackedLeg.getAverageAbsoluteCrossTrackError(timePoint, waitForLatestAnalyses); - } catch (NoWindException nwe) { - // leave averageAbsoluteCrossTrackError as null, meaning "unknown" - averageAbsoluteCrossTrackError = null; - } - result.averageAbsoluteCrossTrackErrorInMeters = averageAbsoluteCrossTrackError == null ? null : averageAbsoluteCrossTrackError.getMeters(); - Distance averageSignedCrossTrackError; - try { - averageSignedCrossTrackError = trackedLeg.getAverageSignedCrossTrackError(timePoint, waitForLatestAnalyses); - } catch (NoWindException nwe) { - // leave averageSignedCrossTrackError as null, meaning "unknown" - averageSignedCrossTrackError = null; - } - result.averageSignedCrossTrackErrorInMeters = averageSignedCrossTrackError == null ? null : averageSignedCrossTrackError.getMeters(); - Double speedOverGroundInKnots; - if (trackedLeg.hasFinishedLeg(timePoint)) { - speedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); - } else { - final SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); - speedOverGroundInKnots = speedOverGround == null ? null : speedOverGround.getKnots(); - } - result.currentSpeedOverGroundInKnots = speedOverGroundInKnots == null ? null : speedOverGroundInKnots; - Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint); - result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters(); - Distance distanceTraveledConsideringGateStart = trackedLeg.getDistanceTraveledConsideringGateStart(timePoint); - result.distanceTraveledIncludingGateStartInMeters = distanceTraveledConsideringGateStart == null ? null : distanceTraveledConsideringGateStart.getMeters(); - final Duration estimatedTimeToNextMarkInSeconds = trackedLeg.getEstimatedTimeToNextMark(timePoint, WindPositionMode.EXACT, cache); - result.estimatedTimeToNextWaypointInSeconds = estimatedTimeToNextMarkInSeconds==null?null:estimatedTimeToNextMarkInSeconds.asSeconds(); - result.timeInMilliseconds = time.asMillis(); - result.finished = trackedLeg.hasFinishedLeg(timePoint); - final TimePoint legFinishTime = trackedLeg.getFinishTime(); - result.correctedTotalTime = trackedLeg.hasStartedLeg(timePoint) ? trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getCorrectedTime(trackedLeg.getCompetitor(), - trackedLeg.hasFinishedLeg(timePoint) ? legFinishTime : timePoint, cache) : null; - // fetch the leg gap in own corrected time from the ranking metric - final Duration gapToLeaderInOwnTime = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric(). - getLegGapToLegLeaderInOwnTime(trackedLeg, timePoint, rankingInfo, cache); - result.gapToLeaderInSeconds = gapToLeaderInOwnTime == null ? null : gapToLeaderInOwnTime.asSeconds(); - if (result.gapToLeaderInSeconds != null) { - final Duration gapAtEndOfPreviousLeg = getGapAtEndOfPreviousLeg(trackedLeg, rankingInfo, cache); - if (gapAtEndOfPreviousLeg != null) { - result.gapChangeSinceLegStartInSeconds = result.gapToLeaderInSeconds - gapAtEndOfPreviousLeg.asSeconds(); - } - } - LinkedHashMap legRanks = legRanksCache.get(trackedLeg.getLeg()); - if (legRanks != null) { - result.rank = legRanks.get(trackedLeg.getCompetitor()); - } else { - result.rank = trackedLeg.getRank(timePoint, cache); - } - result.started = trackedLeg.hasStartedLeg(timePoint); - Speed velocityMadeGood; - if (trackedLeg.hasFinishedLeg(timePoint)) { - velocityMadeGood = trackedLeg.getAverageVelocityMadeGood(timePoint); - } else { - velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint, WindPositionMode.EXACT); - } - result.velocityMadeGoodInKnots = velocityMadeGood == null ? null : velocityMadeGood.getKnots(); - Distance windwardDistanceToGo = trackedLeg.getWindwardDistanceToGo(timePoint, WindPositionMode.LEG_MIDDLE); - result.windwardDistanceToGoInMeters = windwardDistanceToGo == null ? null : windwardDistanceToGo - .getMeters(); - final TimePoint startOfRace = trackedLeg.getTrackedLeg().getTrackedRace().getStartOfRace(); - if (startOfRace != null && trackedLeg.hasStartedLeg(timePoint)) { - // not using trackedLeg.getManeuvers(...) because it may not catch the mark passing maneuver starting this leg - // because that may have been detected as slightly before the mark passing time, hence associated with the previous leg - List maneuvers = trackedLeg.getTrackedLeg().getTrackedRace() - .getManeuvers(trackedLeg.getCompetitor(), startOfRace, timePoint, waitForLatestAnalyses); - if (maneuvers != null) { - result.numberOfManeuvers = new HashMap(); - result.numberOfManeuvers.put(ManeuverType.TACK, 0); - result.numberOfManeuvers.put(ManeuverType.JIBE, 0); - result.numberOfManeuvers.put(ManeuverType.PENALTY_CIRCLE, 0); - Map totalManeuverLossInMeters = new HashMap(); - totalManeuverLossInMeters.put(ManeuverType.TACK, 0.0); - totalManeuverLossInMeters.put(ManeuverType.JIBE, 0.0); - totalManeuverLossInMeters.put(ManeuverType.PENALTY_CIRCLE, 0.0); - TimePoint startOfLeg = trackedLeg.getStartTime(); - for (Maneuver maneuver : maneuvers) { - // don't count maneuvers that were in previous legs - switch (maneuver.getType()) { - case TACK: - case JIBE: - case PENALTY_CIRCLE: - if (!maneuver.getTimePoint().before(startOfLeg) && (legFinishTime == null || legFinishTime.after(timePoint) || - maneuver.getTimePoint().before(legFinishTime))) { - if (maneuver.getManeuverLoss() != null) { - result.numberOfManeuvers.put(maneuver.getType(), - result.numberOfManeuvers.get(maneuver.getType()) + 1); - totalManeuverLossInMeters.put(maneuver.getType(), - totalManeuverLossInMeters.get(maneuver.getType()) - + maneuver.getManeuverLoss().getMeters()); - } - } - break; - case MARK_PASSING: - // analyze all mark passings, not only those after this leg's start, to catch the mark passing - // maneuver starting this leg, even if its time point is slightly before the mark passing starting this leg - MarkPassingManeuver mpm = (MarkPassingManeuver) maneuver; - if (mpm.getWaypointPassed() == trackedLeg.getLeg().getFrom()) { - result.sideToWhichMarkAtLegStartWasRounded = mpm.getSide(); - } - break; - default: - /* Do nothing here. - * Throwing an exception destroys the toggling (and maybe other behaviour) of the leaderboard. - */ - } - } - result.averageManeuverLossInMeters = new HashMap(); - for (ManeuverType maneuverType : new ManeuverType[] { ManeuverType.TACK, ManeuverType.JIBE, - ManeuverType.PENALTY_CIRCLE }) { - if (result.numberOfManeuvers.get(maneuverType) != 0) { - result.averageManeuverLossInMeters.put( - maneuverType, - totalManeuverLossInMeters.get(maneuverType) - / result.numberOfManeuvers.get(maneuverType)); - } - } - } - } - } - return result; - } - - private Duration getGapAtEndOfPreviousLeg(TrackedLegOfCompetitor trackedLeg, final RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { - final Duration result; - final Course course = trackedLeg.getTrackedLeg().getTrackedRace().getRace().getCourse(); - // if trackedLeg is the first leg, compute the gap at the start of this leg; otherwise, compute gap - // at the end of the previous leg - final TimePoint timePoint = trackedLeg.getStartTime(); - final TrackedLegOfCompetitor tloc; - if (course.getFirstWaypoint() == trackedLeg.getLeg().getFrom()) { - tloc = trackedLeg; - } else { - tloc = trackedLeg.getTrackedLeg().getTrackedRace().getTrackedLegFinishingAt(trackedLeg.getLeg().getFrom()) - .getTrackedLeg(trackedLeg.getCompetitor()); - } - result = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getLegGapToLegLeaderInOwnTime(tloc, timePoint, rankingInfo, cache); - return result; - } - - private LeaderboardDTO getLiveLeaderboard(Collection namesOfRaceColumnsForWhichToLoadLegDetails, - boolean addOverallDetails, TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, ExecutionException { - LiveLeaderboardUpdater liveLeaderboardUpdater = getLiveLeaderboardUpdater(trackedRegattaRegistry, - baseDomainFactory); - return liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); - } - - private LiveLeaderboardUpdater getLiveLeaderboardUpdater(TrackedRegattaRegistry trackedRegattaRegistry, - DomainFactory baseDomainFactory) { - LiveLeaderboardUpdater result = this.liveLeaderboardUpdater; - if (result == null) { - synchronized (this) { - result = this.liveLeaderboardUpdater; - if (result == null) { - this.liveLeaderboardUpdater = new LiveLeaderboardUpdater(this, trackedRegattaRegistry, baseDomainFactory); - result = this.liveLeaderboardUpdater; - } - } - } - return result; - } - - private LeaderboardDTOCache getLeaderboardDTOCache() { - LeaderboardDTOCache result = this.leaderboardDTOCache; - if (result == null) { - synchronized (this) { - result = this.leaderboardDTOCache; - if (result == null) { - // The leaderboard cache is invalidated upon all competitor and mark position changes; some analyzes - // are pretty expensive, such as the maneuver re-calculation. Waiting for the latest analysis after only a - // single fix was updated is too expensive if users use the replay feature while a race is still running. - // Therefore, using waitForLatestAnalyses==false seems appropriate here. - this.leaderboardDTOCache = new LeaderboardDTOCache(/* waitForLatestAnalyses */false, this); - result = this.leaderboardDTOCache; - } - } - } - return result; - } - - @Override - public LeaderboardDTO getLeaderboardDTO(TimePoint timePoint, - Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, - TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory, boolean fillNetPointsUncorrected) throws NoWindException, - InterruptedException, ExecutionException { - LeaderboardDTO result = null; - if (timePoint == null) { - // timePoint==null means live mode; however, if we're after the end of all races and after all score - // corrections, don't use the live leaderboard updater which would keep re-calculating over and over again, but map - // this to a usual non-live call which uses the regular LeaderboardDTOCache which is invalidated properly - // when the tracked race associations or score corrections or tracked race contents changes: - final TimePoint nowMinusDelay = this.getNowMinusDelay(); - final TimePoint timePointOfLatestModification = this.getTimePointOfLatestModification(); - if (fillNetPointsUncorrected || (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification))) { - // if there hasn't been any modification to the leaderboard since nowMinusDelay, use non-live mode - // and pull the result from the regular leaderboard cache: - timePoint = timePointOfLatestModification; - } else { - // don't use the regular leaderboard cache; the race still seems to be on; use the live leaderboard updater instead: - timePoint = null; - result = this.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, trackedRegattaRegistry, baseDomainFactory); - } - } - if (timePoint != null) { - if (fillNetPointsUncorrected) { - // explicitly filling the uncorrected net points requires uncached recalculation - result = computeDTO(timePoint, namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, /* waitForLatestAnalyses */ true, - trackedRegattaRegistry, baseDomainFactory, fillNetPointsUncorrected); - } else { - // in replay we'd like up-to-date results; they are still cached - // which is OK because the cache is invalidated whenever any of the tracked races attached to the - // leaderboard changes. - result = getLeaderboardDTOCache().getLeaderboardByName(timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, baseDomainFactory, - trackedRegattaRegistry); - } - } - return result; - } - - public String toString() { - return getName() + " " + (getDefaultCourseArea() != null ? getDefaultCourseArea().getName() : "") + " " + (getScoringScheme() != null ? getScoringScheme().getType().name() : ""); - } - - @Override - public NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher() { - return new NumberOfCompetitorsFetcherImpl(); - } -} +package com.sap.sailing.domain.leaderboard.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +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.NavigableSet; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.abstractlog.race.InvalidatesLeaderboardCache; +import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Leg; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnInSeries; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.ManeuverType; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.NoWindError; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.FleetDTO; +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.MetaLeaderboardRaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.NumberOfCompetitorsInLeaderboardFetcher; +import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.ScoreCorrection.Result; +import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCache; +import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; +import com.sap.sailing.domain.leaderboard.caching.LiveLeaderboardUpdater; +import com.sap.sailing.domain.leaderboard.meta.MetaLeaderboardColumn; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.ranking.RankingMetric.CompetitorRankingInfo; +import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.Maneuver; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.MarkPassingManeuver; +import com.sap.sailing.domain.tracking.RaceChangeListener; +import com.sap.sailing.domain.tracking.TrackedLeg; +import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.WindLegTypeAndLegBearingCache; +import com.sap.sailing.domain.tracking.WindPositionMode; +import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; +import com.sap.sailing.util.impl.RaceColumnListeners; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.concurrent.LockUtil; +import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.util.impl.ThreadFactoryWithPriority; + +/** + * Base implementation for various types of leaderboards. The {@link RaceColumnListener} implementation forwards events + * received to all {@link RaceColumnListener} subscribed with this leaderboard. To which objects this leaderboard subscribes + * as {@link RaceColumnListener} is left to the concrete subclasses to implement, but the race columns seem like useful + * candidates. + * + * @author Axel Uhl (d043530) + * + */ +public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, RaceColumnListener { + private static final long serialVersionUID = 330156778603279333L; + + private static final Logger logger = Logger.getLogger(AbstractSimpleLeaderboardImpl.class.getName()); + + static final Double DOUBLE_0 = new Double(0); + + private final SettableScoreCorrection scoreCorrection; + + private ThresholdBasedResultDiscardingRule crossLeaderboardResultDiscardingRule; + + /** + * The optional display name mappings for competitors. This allows a user to override the tracking-provided + * competitor names for display in a leaderboard. + */ + private final Map displayNames; + + /** the display name of the leaderboard */ + private String displayName; + + /** + * Backs the {@link #getCarriedPoints(Competitor)} API with data. Can be used to prime this leaderboard + * with aggregated results of races not tracked / displayed by this leaderboard in detail. The points + * provided by this map are considered by {@link #getTotalPoints(Competitor, TimePoint)}. + */ + private final Map carriedPoints; + + private final RaceColumnListeners raceColumnListeners; + + /** + * A set that manages the difference between {@link #getCompetitors()} and {@link #getAllCompetitors()}. Access + * is controlled by the {@link #suppressedCompetitorsLock} lock. + */ + private final Set suppressedCompetitors; + private final NamedReentrantReadWriteLock suppressedCompetitorsLock; + + private transient Map, RunnableFuture> raceDetailsAtEndOfTrackingCache; + + /** + * This executor needs to be a different one than {@link #executor} because the tasks run by {@link #executor} + * can depend on the results of the tasks run by {@link #raceDetailsExecutor}, and an {@link Executor} doesn't + * move a task that is blocked by waiting for another {@link FutureTask} to the side but blocks permanently, + * ending in a deadlock (one that cannot easily be detected by the Eclipse debugger either). + */ + private transient Executor raceDetailsExecutor; + + /** + * Used to remove all these listeners from their tracked races when this servlet is {@link #destroy() destroyed}. + */ + private transient Set cacheInvalidationListeners; + + private transient ThreadPoolExecutor executor; + + private transient LiveLeaderboardUpdater liveLeaderboardUpdater; + + private transient LeaderboardDTOCache leaderboardDTOCache; + + /** + * A leaderboard entry representing a snapshot of a cell at a given time point for a single race/competitor. + * + * @author Axel Uhl (d043530) + * + */ + class EntryImpl implements Entry { + private final Callable trackedRankProvider; + private final Double netPoints; + private final Callable netPointsUncorrectedProvider; + private final boolean isNetPointsCorrected; + private final Double totalPoints; + private final MaxPointsReason maxPointsReason; + private final boolean discarded; + private final Fleet fleet; + + private EntryImpl(Callable trackedRankProvider, Double netPoints, + Callable netPointsUncorrectedProvider, boolean isNetPointsCorrected, + Double totalPoints, MaxPointsReason maxPointsReason, boolean discarded, Fleet fleet) { + super(); + this.trackedRankProvider = trackedRankProvider; + this.netPoints = netPoints; + this.netPointsUncorrectedProvider = netPointsUncorrectedProvider; + this.isNetPointsCorrected = isNetPointsCorrected; + this.totalPoints = totalPoints; + this.maxPointsReason = maxPointsReason; + this.discarded = discarded; + this.fleet = fleet; + } + @Override + public int getTrackedRank() { + try { + return trackedRankProvider.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @Override + public Double getNetPoints() { + return netPoints; + } + @Override + public boolean isNetPointsCorrected() { + return isNetPointsCorrected; + } + @Override + public Double getTotalPoints() { + return totalPoints; + } + @Override + public MaxPointsReason getMaxPointsReason() { + return maxPointsReason; + } + @Override + public boolean isDiscarded() { + return discarded; + } + @Override + public Fleet getFleet() { + return fleet; + } + @Override + public Double getNetPointsUncorrected() { + try { + return netPointsUncorrectedProvider.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + /** + * Computing the competitors can be a bit expensive, particularly if the fleet is large and there may be suppressed + * competitors, and the leaderboard may be a meta-leaderboard that refers to other leaderboards which each have + * several tracked races attached from where the competitors need to be retrieved. Ideally, the competitors list + * would be cached, but that is again difficult because we would have to monitor all changes in all dependent + * leaderboards and columns and tracked races properly. + *

    + * + * As it turns out, one of the most frequent uses of the {@link AbstractSimpleLeaderboardImpl#getCompetitors} + * competitors list is to determine their number which in turn is only required for high-point scoring systems and + * for computing the default score for penalties. Again, the most frequently used low-point family of scoring schemes + * does not require this number. Yet, the scoring scheme requires an argument for polymorphic use by those that + * need it. Instead of computing it for each call, this interface lets us defer the actual calculation until the + * point when it's really needed. Once asked, this object will cache the result. Therefore, a new one should be + * constructed each time the number shall be computed. + * + * @author Axel Uhl (D043530) + * + */ + public class NumberOfCompetitorsFetcherImpl implements NumberOfCompetitorsInLeaderboardFetcher { + private int numberOfCompetitors = -1; + + @Override + public int getNumberOfCompetitorsInLeaderboard() { + if (numberOfCompetitors == -1) { + numberOfCompetitors = Util.size(getCompetitors()); + } + return numberOfCompetitors; + } + } + + /** + * Handles the invalidation of the {@link SailingServiceImpl#raceDetailsAtEndOfTrackingCache} entries if the tracked + * race changes in any way. In particular, for {@link #statusChanged}, when the status changes away from LOADING, + * calculations may start or resume, making it necessary to clear the cache. + * + * @author Axel Uhl (D043530) + * + */ + private class CacheInvalidationListener extends AbstractRaceChangeListener { + private final TrackedRace trackedRace; + private final Competitor competitor; + + public CacheInvalidationListener(TrackedRace trackedRace, Competitor competitor) { + this.trackedRace = trackedRace; + this.competitor = competitor; + } + + public TrackedRace getTrackedRace() { + return trackedRace; + } + + public void removeFromTrackedRace() { + trackedRace.removeListener(this); + } + + private void invalidateCacheAndRemoveThisListenerFromTrackedRace() { + synchronized (raceDetailsAtEndOfTrackingCache) { + raceDetailsAtEndOfTrackingCache.remove(new com.sap.sse.common.Util.Pair(trackedRace, competitor)); + removeFromTrackedRace(); + } + } + + @Override + protected void defaultAction() { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + } + + private static class UUIDGenerator implements LeaderboardDTO.UUIDGenerator { + @Override + public String generateRandomUUID() { + return UUID.randomUUID().toString(); + } + } + + public AbstractSimpleLeaderboardImpl(ThresholdBasedResultDiscardingRule resultDiscardingRule) { + this.carriedPoints = new HashMap(); + this.scoreCorrection = createScoreCorrection(); + this.displayNames = new HashMap(); + this.crossLeaderboardResultDiscardingRule = resultDiscardingRule; + this.suppressedCompetitors = new HashSet(); + this.suppressedCompetitorsLock = new NamedReentrantReadWriteLock("suppressedCompetitorsLock", /* fair */ false); + this.raceColumnListeners = new RaceColumnListeners(); + this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); + initTransientFields(); + } + + /** + * Produces the score correction object to use in this leaderboard. Used by the constructor. Subclasses may override + * this method to create a more specific type of score correction. This implementation produces an object of type + * {@link ScoreCorrectionImpl}. + */ + protected SettableScoreCorrection createScoreCorrection() { + return new ScoreCorrectionImpl(this); + } + + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + initTransientFields(); + } + + private void initTransientFields() { + this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); + this.raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + this.cacheInvalidationListeners = new HashSet(); + // When many updates are triggered in a short period of time by a single thread, ensure that the single thread + // providing the updates is not outperformed by all the re-calculations happening here. Leave at least one + // core to other things, but by using at least three threads ensure that no simplistic deadlocks may occur. + final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3); + executor = new ThreadPoolExecutor(/* corePoolSize */ THREAD_POOL_SIZE, + /* maximumPoolSize */ THREAD_POOL_SIZE, + /* keepAliveTime */ 60, TimeUnit.SECONDS, + /* workQueue */ new LinkedBlockingQueue(), new ThreadFactoryWithPriority(Thread.NORM_PRIORITY-1, /* daemon */ true)); + } + + @Override + public void destroy() { + for (CacheInvalidationListener cacheInvalidationListener : cacheInvalidationListeners) { + cacheInvalidationListener.removeFromTrackedRace(); + } + } + + @Override + public SettableScoreCorrection getScoreCorrection() { + return scoreCorrection; + } + + @Override + public String getDisplayName(Competitor competitor) { + return displayNames.get(competitor); + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @Override + public ResultDiscardingRule getResultDiscardingRule() { + return crossLeaderboardResultDiscardingRule; + } + + @Override + public void setCarriedPoints(Competitor competitor, double carriedPoints) { + Double oldCarriedPoints = this.carriedPoints.put(competitor, carriedPoints); + if (!Util.equalsWithNull(oldCarriedPoints, carriedPoints)) { + getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, carriedPoints); + } + } + + @Override + public double getCarriedPoints(Competitor competitor) { + Double result = carriedPoints.get(competitor); + return result == null ? 0 : result; + } + + @Override + public Map getCompetitorsForWhichThereAreCarriedPoints() { + return Collections.unmodifiableMap(carriedPoints); + } + + @Override + public void unsetCarriedPoints(Competitor competitor) { + Double oldCarriedPoints = carriedPoints.remove(competitor); + if (oldCarriedPoints != null) { + getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, null); + } + } + + @Override + public boolean hasCarriedPoints() { + return !carriedPoints.isEmpty(); + } + + @Override + public boolean hasCarriedPoints(Competitor competitor) { + return carriedPoints.containsKey(competitor); + } + + @Override + public void setDisplayName(Competitor competitor, String displayName) { + String oldDisplayName = displayNames.get(competitor); + displayNames.put(competitor, displayName); + if (!Util.equalsWithNull(oldDisplayName, displayName)) { + getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); + } + } + + @Override + public void setCrossLeaderboardResultDiscardingRule(ThresholdBasedResultDiscardingRule discardingRule) { + ResultDiscardingRule oldDiscardingRule = getResultDiscardingRule(); + this.crossLeaderboardResultDiscardingRule = discardingRule; + getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, discardingRule); + } + + @Override + public Double getNetPoints(final Competitor competitor, final RaceColumn raceColumn, final TimePoint timePoint) { + return getScoreCorrection().getCorrectedScore( + new Callable() { + public Integer call() throws NoWindException { + return getTrackedRank(competitor, raceColumn, timePoint); + } + }, competitor, + raceColumn, timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()).getCorrectedScore(); + } + + @Override + public MaxPointsReason getMaxPointsReason(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + return getScoreCorrection().getMaxPointsReason(competitor, raceColumn, timePoint); + } + + @Override + public boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + return isDiscarded(competitor, raceColumn, getRaceColumns(), timePoint); + } + + private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, + Iterable raceColumnsToConsider, TimePoint timePoint) { + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + return isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns); + } + + /** + * Same as {@link #isDiscarded(Competitor, RaceColumn, TimePoint)}, only that the set of discarded race columns can + * be specified which is useful when total points are to be computed for more than one column for the same + * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done + * once and not again for each column (which would lead to quadratic effort). + * + * @param discardedRaceColumns + * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. + * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) + * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. + */ + private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, + final Set discardedRaceColumns) { + return !raceColumn.isMedalRace() + && getMaxPointsReason(competitor, raceColumn, timePoint).isDiscardable() + && discardedRaceColumns.contains(raceColumn); + } + + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { + return getTotalPoints(competitor, raceColumn, getRaceColumns(), timePoint); + } + + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, + Iterable raceColumnsToConsider, TimePoint timePoint) throws NoWindException { + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + return getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); + } + + /** + * Same as {@link #getTotalPoints(Competitor, RaceColumn, Iterable, TimePoint)}, only that the set of discarded race columns can + * be specified which is useful when total points are to be computed for more than one column for the same + * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done + * once and not again for each column (which would lead to quadratic effort). + * + * @param discardedRaceColumns + * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. + * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) + * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. + */ + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, Set discardedRaceColumns) { + Double result; + if (isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns)) { + result = 0.0; + } else { + final Double netPoints = getNetPoints(competitor, raceColumn, timePoint); + if (netPoints == null) { + result = null; + } else { + result = raceColumn.getFactor() * netPoints; + } + } + return result; + } + + @Override + public Double getTotalPoints(Competitor competitor, TimePoint timePoint) { + return getTotalPoints(competitor, getRaceColumns(), timePoint); + } + + @Override + public Double getTotalPoints(Competitor competitor, final Iterable raceColumnsToConsider, + TimePoint timePoint) { + // when a column with isStartsWithZeroScore() is found, only reset score if the competitor scored in any race from there on + boolean needToResetScoreUponNextNonEmptyEntry = false; + double result = getCarriedPoints(competitor); + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + for (RaceColumn raceColumn : raceColumnsToConsider) { + if (raceColumn.isStartsWithZeroScore()) { + needToResetScoreUponNextNonEmptyEntry = true; + } + if (getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint)) { + final Double totalPoints = getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); + if (totalPoints != null) { + if (needToResetScoreUponNextNonEmptyEntry) { + result = 0; + needToResetScoreUponNextNonEmptyEntry = false; + } + result += totalPoints; + } + } + } + return result; + } + + /** + * All competitors with non-null net points are added to the result which is then sorted by net points in ascending + * order. The fleet, if ordered, is the primary ordering criterion, followed by the net points. + */ + @Override + public List getCompetitorsFromBestToWorst(final RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { + final Map> netPointsAndFleet = new HashMap>(); + for (Competitor competitor : getCompetitors()) { + Double netPoints = getNetPoints(competitor, raceColumn, timePoint); + if (netPoints != null) { + netPointsAndFleet.put(competitor, new com.sap.sse.common.Util.Pair(netPoints, raceColumn.getFleetOfCompetitor(competitor))); + } + } + List result = new ArrayList(netPointsAndFleet.keySet()); + Collections.sort(result, new Comparator() { + @Override + public int compare(Competitor o1, Competitor o2) { + int comparisonResult; + if (o1 == o2) { + comparisonResult = 0; + } else { + if (raceColumn.hasSplitFleets() && !raceColumn.hasSplitFleetContiguousScoring()) { + // only check fleets if there are more than one and the column is not to be contiguously scored even in case + // of split fleets + final Fleet o1Fleet = netPointsAndFleet.get(o1).getB(); + final Fleet o2Fleet = netPointsAndFleet.get(o2).getB(); + if (o1Fleet == null) { + if (o2Fleet == null) { + comparisonResult = 0; + } else { + comparisonResult = 1; // o1 ranks "worse" because it doesn't have a fleet set while o2 + // has + } + } else { + if (o2Fleet == null) { + comparisonResult = -1; // o1 ranks "better" because it has a fleet set while o2 hasn't + } else { + comparisonResult = o1Fleet.compareTo(o2Fleet); + } + } + } else { + // either there are no split fleets or the split isn't relevant for scoring as for ordered fleets + // the scoring runs contiguously from top to bottom + comparisonResult = 0; + } + if (comparisonResult == 0) { + comparisonResult = getScoringScheme().getScoreComparator(/* nullScoresAreBetter */ false).compare( + netPointsAndFleet.get(o1).getA(), netPointsAndFleet.get(o2).getA()); + } + } + return comparisonResult; + } + }); + return result; + } + + @Override + public List getCompetitorsFromBestToWorst(TimePoint timePoint) { + return getCompetitorsFromBestToWorst(getRaceColumns(), timePoint); + } + + private List getCompetitorsFromBestToWorst(Iterable raceColumnsToConsider, TimePoint timePoint) { + List result = new ArrayList(); + for (Competitor competitor : getCompetitors()) { + result.add(competitor); + } + Collections.sort(result, getTotalRankComparator(raceColumnsToConsider, timePoint)); + return result; + } + + @Override + public int getTotalRankOfCompetitor(Competitor competitor, TimePoint timePoint) throws NoWindException { + List competitorsFromBestToWorst = getCompetitorsFromBestToWorst(timePoint); + return competitorsFromBestToWorst.indexOf(competitor) + 1; + } + + protected Comparator getTotalRankComparator(Iterable raceColumnsToConsider, TimePoint timePoint) { + return new LeaderboardTotalRankComparator(this, timePoint, getScoringScheme(), /* nullScoresAreBetter */ false, raceColumnsToConsider); + } + + @Override + public RaceColumn getRaceColumnByName(String columnName) { + RaceColumn result = null; + for (RaceColumn r : getRaceColumns()) { + if (r.getName().equals(columnName)) { + result = r; + break; + } + } + return result; + } + + @Override + public Competitor getCompetitorByName(String competitorName) { + for (Competitor competitor : getAllCompetitors()) { + if (competitor.getName().equals(competitorName)) { + return competitor; + } + } + return null; + } + + @Override + public boolean countRaceForComparisonWithDiscardingThresholds(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + TrackedRace trackedRaceForCompetitorInColumn; + return getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint) && + (getScoreCorrection().isScoreCorrected(competitor, raceColumn, timePoint) || + ((trackedRaceForCompetitorInColumn=raceColumn.getTrackedRace(competitor)) != null && + trackedRaceForCompetitorInColumn.hasStarted(timePoint) && + trackedRaceForCompetitorInColumn.getRank(competitor, timePoint) != 0)); + } + + @Override + public void addRaceColumnListener(RaceColumnListener listener) { + getRaceColumnListeners().addRaceColumnListener(listener); + } + + @Override + public void removeRaceColumnListener(RaceColumnListener listener) { + getRaceColumnListeners().removeRaceColumnListener(listener); + } + + @Override + public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint) throws NoWindException { + final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + return getEntry(competitor, race, timePoint, discardedRaceColumns); + } + + @Override + public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint, + Set discardedRaceColumns) throws NoWindException { + Callable trackedRankProvider = new Callable() { + public Integer call() throws NoWindException { + return getTrackedRank(competitor, race, timePoint); + } + }; + final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, race, + timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); + boolean discarded = isDiscarded(competitor, race, timePoint, discardedRaceColumns); + final Double correctedScore = correctedResults.getCorrectedScore(); + return new EntryImpl(trackedRankProvider, correctedScore, new Callable() { + @Override + public Double call() { + return correctedResults.getUncorrectedScore(); + } + }, correctedResults.isCorrected(), discarded ? DOUBLE_0 : correctedScore == null ? null + : Double.valueOf(correctedScore * race.getFactor()), correctedResults.getMaxPointsReason(), discarded, + race.getFleetOfCompetitor(competitor)); + } + + @Override + public Map> getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(TimePoint timePoint) throws NoWindException { + Map> result = new LinkedHashMap<>(); + List raceColumnsToConsider = new ArrayList<>(); + for (RaceColumn raceColumn : getRaceColumns()) { + raceColumnsToConsider.add(raceColumn); + result.put(raceColumn, getCompetitorsFromBestToWorst(raceColumnsToConsider, timePoint)); + } + return result; + } + + @Override + public Map> getTotalPointsSumAfterRaceColumn(final TimePoint timePoint) + throws NoWindException { + final Map> result = new LinkedHashMap<>(); + List raceColumnsToConsider = new ArrayList<>(); + Map>> futures = new HashMap<>(); + for (final RaceColumn raceColumn : getRaceColumns()) { + raceColumnsToConsider.add(raceColumn); + final Iterable finalRaceColumnsToConsider = new ArrayList<>(raceColumnsToConsider); + futures.put(raceColumn, executor.submit(new Callable>() { + @Override + public Map call() { + Map totalPointsSumPerCompetitorInColumn = new HashMap<>(); + for (Competitor competitor : getCompetitors()) { + totalPointsSumPerCompetitorInColumn.put(competitor, getTotalPoints(competitor, finalRaceColumnsToConsider, timePoint)); + } + synchronized (result) { + return totalPointsSumPerCompetitorInColumn; + } + } + })); + } + for (RaceColumn raceColumn : getRaceColumns()) { + try { + result.put(raceColumn, futures.get(raceColumn).get()); + } catch (InterruptedException | ExecutionException e) { + if (e.getCause() instanceof NoWindError) { + throw ((NoWindError) e.getCause()).getCause(); + } else { + throw new RuntimeException(e); // no caught exceptions occur in the futures executed + } + } + } + return result; + } + + @Override + public Map, Entry> getContent(final TimePoint timePoint) throws NoWindException { + Map, Entry> result = new HashMap, Entry>(); + Map> discardedRaces = new HashMap>(); + for (final RaceColumn raceColumn : getRaceColumns()) { + for (final Competitor competitor : getCompetitors()) { + Callable trackedRankProvider = new Callable() { + @Override + public Integer call() throws Exception { + return getTrackedRank(competitor, raceColumn, timePoint); + } + }; + final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, raceColumn, + timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); + Set discardedRacesForCompetitor = discardedRaces.get(competitor); + if (discardedRacesForCompetitor == null) { + discardedRacesForCompetitor = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + discardedRaces.put(competitor, discardedRacesForCompetitor); + } + boolean discarded = discardedRacesForCompetitor.contains(raceColumn); + final Double correctedScore = correctedResults.getCorrectedScore(); + Entry entry = new EntryImpl(trackedRankProvider, correctedScore, + new Callable() { @Override public Double call() { return correctedResults.getUncorrectedScore(); } }, + correctedResults.isCorrected(), + discarded ? DOUBLE_0 : (correctedScore==null?null: + Double.valueOf((correctedScore * raceColumn.getFactor()))), correctedResults.getMaxPointsReason(), + discarded, raceColumn.getFleetOfCompetitor(competitor)); + result.put(new com.sap.sse.common.Util.Pair(competitor, raceColumn), entry); + } + } + return result; + } + + @Override + public void trackedRaceLinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); + } + + @Override + public void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); + // It's generally possible that a leaderboard links to the same tracked race in multiple columns / fleets; + // only if it no longer references the trackedRace currently unlinked from one column/fleet, also unlink + // all cache invalidation listeners for said trackedRace + if (!Util.contains(getTrackedRaces(), trackedRace)) { + synchronized (cacheInvalidationListeners) { + for (Iterator cacheInvalidationListenerIter=cacheInvalidationListeners.iterator(); + cacheInvalidationListenerIter.hasNext(); ) { + CacheInvalidationListener cacheInvalidationListener = cacheInvalidationListenerIter.next(); + if (cacheInvalidationListener.getTrackedRace() == trackedRace) { + cacheInvalidationListener.removeFromTrackedRace(); + cacheInvalidationListenerIter.remove(); + } + } + } + } + } + + @Override + public void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { + getRaceColumnListeners().notifyListenersAboutIsMedalRaceChanged(raceColumn, newIsMedalRace); + } + + @Override + public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { + getRaceColumnListeners().notifyListenersAboutIsStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); + } + + @Override + public void isFirstColumnIsNonDiscardableCarryForwardChanged(RaceColumn raceColumn, boolean firstColumnIsNonDiscardableCarryForward) { + getRaceColumnListeners().notifyListenersAboutIsFirstColumnIsNonDiscardableCarryForwardChanged(raceColumn, firstColumnIsNonDiscardableCarryForward); + } + + @Override + public void hasSplitFleetContiguousScoringChanged(RaceColumn raceColumn, boolean hasSplitFleetContiguousScoring) { + getRaceColumnListeners().notifyListenersAboutHasSplitFleetContiguousScoringChanged(raceColumn, hasSplitFleetContiguousScoring); + } + + @Override + public void raceColumnMoved(RaceColumn raceColumn, int newIndex) { + getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(raceColumn, newIndex); + } + + @Override + public void factorChanged(RaceColumn raceColumn, Double oldFactor, Double newFactor) { + getRaceColumnListeners().notifyListenersAboutFactorChanged(raceColumn, oldFactor, newFactor); + } + + /** + * A leaderboard will only accept the addition of a race column if the column's name is unique across the leaderboard. + */ + @Override + public boolean canAddRaceColumnToContainer(RaceColumn newRaceColumn) { + boolean result = true; + for (RaceColumn raceColumn : getRaceColumns()) { + if (raceColumn.getName().equals(newRaceColumn.getName())) { + result = false; + break; + } + } + return result; + } + + @Override + public void raceColumnAddedToContainer(RaceColumn raceColumn) { + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(raceColumn); + } + + @Override + public void raceColumnRemovedFromContainer(RaceColumn raceColumn) { + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); + } + + @Override + public void competitorDisplayNameChanged(Competitor competitor, String oldDisplayName, String displayName) { + getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); + } + + @Override + public void resultDiscardingRuleChanged(ResultDiscardingRule oldDiscardingRule, ResultDiscardingRule newDiscardingRule) { + getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, newDiscardingRule); + } + + @Override + public boolean isTransient() { + return false; + } + + /** + * Finds out the time point when any of the {@link Leaderboard#getTrackedRaces() tracked races currently attached to + * the leaderboard} and the {@link Leaderboard#getScoreCorrection() score corrections} have last been + * modified. If no tracked race is attached and no time-stamped score corrections have been applied to the leaderboard, + * null is returned. The time point computed this way is a good choice for normalizing queries for later time + * points in an attempt to achieve more cache hits.

    + * + * Note, however, that the result does not tell about structural changes to the leaderboard and therefore cannot be used + * to determine the need for cache invalidation. For example, if a column is added to a leaderboard after the time point + * returned by this method but that column's attached tracked race has finished before the time point returned by this method, + * the result of this method won't change. Still, the contents of the leaderboard will change by a change in column structure. + * A different means to determine the possibility of changes that happened to this leaderboard must be used for cache + * management. Such a facility has to listen for score correction changes, tracked races being attached or detached and + * the column structure changing. + * + * @see TrackedRace#getTimePointOfNewestEvent() + * @see SettableScoreCorrection#getTimePointOfLastCorrectionsValidity() + */ + @Override + public TimePoint getTimePointOfLatestModification() { + TimePoint result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + if (result == null || (trackedRace.getTimePointOfNewestEvent() != null && trackedRace.getTimePointOfNewestEvent().after(result))) { + result = trackedRace.getTimePointOfNewestEvent(); + } + } + TimePoint timePointOfLastScoreCorrection = getScoreCorrection().getTimePointOfLastCorrectionsValidity(); + if (timePointOfLastScoreCorrection != null && (result == null || timePointOfLastScoreCorrection.after(result))) { + result = timePointOfLastScoreCorrection; + } + return result; + } + + @Override + public com.sap.sse.common.Util.Pair getMaximumSpeedOverGround(Competitor competitor, TimePoint timePoint) { + com.sap.sse.common.Util.Pair result = null; + // TODO should we ensure that competitor participated in all race columns? + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + NavigableSet markPassings = trackedRace.getMarkPassings(competitor); + if (!markPassings.isEmpty()) { + TimePoint from = markPassings.first().getTimePoint(); + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) && + markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + to = timePoint; + } + com.sap.sse.common.Util.Pair maxSpeed = trackedRace.getTrack(competitor).getMaximumSpeedOverGround(from, to); + if (result == null || result.getB() == null || + (maxSpeed != null && maxSpeed.getB() != null && maxSpeed.getB().compareTo(result.getB()) > 0)) { + result = maxSpeed; + } + } + } + } + return result; + } + + @Override + public Speed getAverageSpeedOverGround(Competitor competitor, TimePoint timePoint) { + Speed result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + NavigableSet markPassings = trackedRace.getMarkPassings(competitor); + if (!markPassings.isEmpty()) { + TimePoint from = markPassings.first().getTimePoint(); + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) && + markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + if (markPassings.last().getWaypoint() != trackedRace.getRace().getCourse().getLastWaypoint() && + timePoint.after(markPassings.last().getTimePoint())) { + result = null; + break; + } + to = timePoint; + } + Distance distanceTraveled = trackedRace.getDistanceTraveled(competitor, timePoint); + if (distanceTraveled != null) { + result = distanceTraveled.inTime(to.asMillis()-from.asMillis()); + } + } + } + } + return result; + } + + @Override + public Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint) throws NoWindException { + return getTotalTimeSailedInLegType(competitor, legType, timePoint, new HashMap()); + } + + private Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint, Map legTypeCache) throws NoWindException { + Duration result = null; + // TODO should we ensure that competitor participated in all race columns? + outerLoop: + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + trackedRace.getRace().getCourse().lockForRead(); + try { + for (Leg leg : trackedRace.getRace().getCourse().getLegs()) { + TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, leg); + if (trackedLegOfCompetitor.hasStartedLeg(timePoint)) { + // find out leg type at the time the competitor started the leg + try { + final TrackedLeg trackedLeg = trackedRace.getTrackedLeg(leg); + LegType trackedLegType = legTypeCache.get(trackedLeg); + if (trackedLegType == null) { + final TimePoint startTime = trackedLegOfCompetitor.getStartTime(); + TimePoint finishTime = trackedLegOfCompetitor.getFinishTime(); + if (finishTime == null) { + finishTime = timePoint; + } + trackedLegType = trackedLeg.getLegType(startTime.plus(startTime.until(finishTime).divide(2))); // middle of the leg + legTypeCache.put(trackedLeg, trackedLegType); + } + if (legType == trackedLegType) { + Duration timeSpentOnDownwind = trackedLegOfCompetitor.getTime(timePoint); + if (timeSpentOnDownwind != null) { + if (result == null) { + result = timeSpentOnDownwind; + } else { + result = result.plus(timeSpentOnDownwind); + } + } else { + // Although the competitor has started the leg, no value was produced. This + // means that the competitor didn't finish the leg before tracking ended. No useful value + // can be obtained for this competitor anymore. + result = null; + break outerLoop; + } + } + } catch (NoWindException nwe) { + // without wind there is no leg type and hence there is no reasonable value for this: + result = null; + break outerLoop; + } + } + } + } finally { + trackedRace.getRace().getCourse().unlockAfterRead(); + } + } + } + return result; + } + + @Override + public Duration getTotalTimeSailed(Competitor competitor, TimePoint timePoint) { + Duration result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + NavigableSet markPassings = trackedRace.getMarkPassings(competitor); + if (!markPassings.isEmpty()) { + TimePoint from = trackedRace.getStartOfRace(); // start counting at race start, not when the competitor passed the line + if (from != null && !timePoint.before(from)) { // but only if the race started after timePoint + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) + && markPassings.last().getWaypoint() == trackedRace.getRace().getCourse() + .getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + if (trackedRace.getEndOfTracking() != null + && timePoint.after(trackedRace.getEndOfTracking())) { + result = null; // race not finished until end of tracking; no reasonable value can be + // computed for competitor + break; + } else { + to = timePoint; + } + } + Duration timeSpent = from.until(to); + if (result == null) { + result = timeSpent; + } else { + result=result.plus(timeSpent); + } + } + } + } + } + return result; + } + + @Override + public Distance getTotalDistanceTraveled(Competitor competitor, TimePoint timePoint) { + Distance result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + TimePoint startOfRace; + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor) && + (startOfRace=trackedRace.getStartOfRace()) != null && + !startOfRace.after(timePoint)) { + Distance distanceSailedInRace = trackedRace.getDistanceTraveled(competitor, timePoint); + if (distanceSailedInRace != null) { + if (result == null) { + result = distanceSailedInRace; + } else { + result = result.add(distanceSailedInRace); + } + } else { + // if competitor has not finished one single race in the whole + // series then we can not return a meaningful value for all + // all races + return null; + } + } + } + return result; + } + + protected RaceColumnListeners getRaceColumnListeners() { + return raceColumnListeners; + } + + @Override + public Iterable getCompetitors() { + final Iterable result; + // mostly the set of suppressed competitors is empty; in this case, avoid having to loop over the + // potentially large set of competitors + if (Util.isEmpty(getSuppressedCompetitors())) { + result = getAllCompetitors(); + } else { + final Iterable allCompetitors = getAllCompetitors(); + final Set suppressed = new HashSet<>(); + Util.addAll(getSuppressedCompetitors(), suppressed); + result = getCompetitorIterableSkippingSuppressed(allCompetitors, suppressed); + } + return result; + } + + @Override + public Iterable getCompetitors(RaceColumn raceColumn, Fleet fleet) { + return getCompetitorIterableSkippingSuppressed(getAllCompetitors(raceColumn, fleet), getSuppressedCompetitors()); + } + + /** + * return an iterable with a smart iterator that filters out the suppressed elements on demand + */ + protected Iterable getCompetitorIterableSkippingSuppressed(final Iterable allCompetitors, + final Iterable suppressed) { + final Iterable result; + result = new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + private final Iterator allIter = allCompetitors.iterator(); + private Competitor next = advance(); + + private Competitor advance() { + next = null; + while (allIter.hasNext() && next == null) { + next = allIter.next(); + if (Util.contains(suppressed, next)) { + next = null; + } + } + return next; + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public Competitor next() { + if (next == null) { + throw new NoSuchElementException(); + } + final Competitor result = next; + advance(); + return result; + } + }; + } + }; + return result; + } + + @Override + public Iterable getSuppressedCompetitors() { + LockUtil.lockForRead(suppressedCompetitorsLock); + try { + return new HashSet(suppressedCompetitors); + } finally { + LockUtil.unlockAfterRead(suppressedCompetitorsLock); + } + } + + @Override + public void setSuppressed(Competitor competitor, boolean suppressed) { + LockUtil.lockForWrite(suppressedCompetitorsLock); + try { + if (suppressed) { + suppressedCompetitors.add(competitor); + } else { + suppressedCompetitors.remove(competitor); + } + } finally { + LockUtil.unlockAfterWrite(suppressedCompetitorsLock); + } + getScoreCorrection().notifyListenersAboutIsSuppressedChange(competitor, suppressed); + } + + @Override + public TimePoint getNowMinusDelay() { + final TimePoint now = MillisecondsTimePoint.now(); + final Long delayToLiveInMillis = getDelayToLiveInMillis(); + TimePoint timePoint = delayToLiveInMillis == null ? now : now.minus(delayToLiveInMillis); + return timePoint; + } + + @Override + public void raceLogEventAdded(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { + getRaceColumnListeners().notifyListenersAboutRaceLogEventAdded(raceColumn, raceLogIdentifier, event); + if (event instanceof InvalidatesLeaderboardCache) { + // make sure to invalidate the cache as this event indicates that + // it changes values the cache could still hold + if (leaderboardDTOCache != null) { + leaderboardDTOCache.invalidate(this); + } + } + } + + @Override + public LeaderboardDTO computeDTO(final TimePoint timePoint, + final Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + final boolean waitForLatestAnalyses, TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory, + final boolean fillNetPointsUncorrected) + throws NoWindException { + long startOfRequestHandling = System.currentTimeMillis(); + final LeaderboardDTOCalculationReuseCache cache = new LeaderboardDTOCalculationReuseCache(timePoint); + final LeaderboardDTO result = new LeaderboardDTO(this.getScoreCorrection().getTimePointOfLastCorrectionsValidity() == null ? null + : this.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(), + this.getScoreCorrection() == null ? null : this.getScoreCorrection().getComment(), + this.getScoringScheme() == null ? null : this.getScoringScheme().getType(), this + .getScoringScheme().isHigherBetter(), new UUIDGenerator(), addOverallDetails); + result.competitors = new ArrayList(); + result.name = this.getName(); + result.competitorDisplayNames = new HashMap(); + for (Competitor suppressedCompetitor : this.getSuppressedCompetitors()) { + result.setSuppressed(baseDomainFactory.convertToCompetitorDTO(suppressedCompetitor), true); + } + // Now create the race columns and, as a future task, set their competitorsFromBestToWorst, then wait for all these + // futures to finish: + Map>> competitorsFromBestToWorstTasks = new HashMap<>(); + for (final RaceColumn raceColumn : this.getRaceColumns()) { + boolean isMetaLeaderboardColumn = raceColumn instanceof MetaLeaderboardColumn; + RaceColumnDTO raceColumnDTO = result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(), + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, + isMetaLeaderboardColumn); + if (isMetaLeaderboardColumn && raceColumnDTO instanceof MetaLeaderboardRaceColumnDTO) { + calculateRacesMetadata((MetaLeaderboardColumn) raceColumn, (MetaLeaderboardRaceColumnDTO) raceColumnDTO, + trackedRegattaRegistry, baseDomainFactory); + } + for (Fleet fleet : raceColumn.getFleets()) { + RegattaAndRaceIdentifier raceIdentifier = null; + RaceDTO race = null; + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + final FleetDTO fleetDTO = baseDomainFactory.convertToFleetDTO(fleet); + if (trackedRace != null) { + raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta().getName(), + trackedRace.getRace().getName()); + race = baseDomainFactory.createRaceDTO(trackedRegattaRegistry, /* withGeoLocationData */ false, raceIdentifier, trackedRace); + } + // Note: the RaceColumnDTO won't be created by the following addRace call because it has been created + // above by the result.createEmptyRaceColumn call + result.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, + fleetDTO, raceColumn.isMedalRace(), raceIdentifier, race, isMetaLeaderboardColumn); + } + FutureTask> task = new FutureTask>( + () -> baseDomainFactory.getCompetitorDTOList(AbstractSimpleLeaderboardImpl.this.getCompetitorsFromBestToWorst(raceColumn, timePoint))); + executor.execute(task); + competitorsFromBestToWorstTasks.put(raceColumn, task); + } + // wait for the competitor orderings to have been computed for all race columns before continuing; subsequent tasks may depend on these data + for (Map.Entry>> raceColumnAndTaskToJoin : competitorsFromBestToWorstTasks.entrySet()) { + try { + result.setCompetitorsFromBestToWorst(raceColumnAndTaskToJoin.getKey().getName(), raceColumnAndTaskToJoin.getValue().get()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept + // null values. + // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So + // this helps robustness from a user's perspective. + logger.log( + Level.SEVERE, + AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " + + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails + + "): exception during computing competitor ordering for race column "+raceColumnAndTaskToJoin.getKey().getName(), e); + } + } + result.setDelayToLiveInMillisForLatestRace(this.getDelayToLiveInMillis()); + result.rows = new HashMap(); + result.hasCarriedPoints = this.hasCarriedPoints(); + if (this.getResultDiscardingRule() instanceof ThresholdBasedResultDiscardingRule) { + result.discardThresholds = ((ThresholdBasedResultDiscardingRule) this.getResultDiscardingRule()) + .getDiscardIndexResultsStartingWithHowManyRaces(); + } else { + result.discardThresholds = null; + } + // Computing the competitor leg ranks is expensive, especially in live mode, in case new events keep + // invalidating the ranks cache in TrackedLegImpl. The problem then is that the sorting based on wind data is repeated for + // each competitor, leading to square effort. We therefore need to compute the leg ranks for those races where leg + // details are requested only once and pass them into getLeaderboardEntryDTO + final Map> legRanksCache = new HashMap>(); + for (final RaceColumn raceColumn : this.getRaceColumns()) { + // if details for the column are requested, cache the leg's ranks + if (namesOfRaceColumnsForWhichToLoadLegDetails != null + && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName())) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + trackedRace.getRace().getCourse().lockForRead(); + try { + for (TrackedLeg trackedLeg : trackedRace.getTrackedLegs()) { + legRanksCache.put(trackedLeg.getLeg(), trackedLeg.getRanks(timePoint, cache)); + } + } finally { + trackedRace.getRace().getCourse().unlockAfterRead(); + } + } + } + } + } + for (final Competitor competitor : this.getCompetitorsFromBestToWorst(timePoint)) { + CompetitorDTO competitorDTO = baseDomainFactory.convertToCompetitorDTO(competitor); + LeaderboardRowDTO row = new LeaderboardRowDTO(); + row.competitor = competitorDTO; + row.fieldsByRaceColumnName = new HashMap(); + row.carriedPoints = this.hasCarriedPoints(competitor) ? this.getCarriedPoints(competitor) : null; + row.totalPoints = this.getTotalPoints(competitor, timePoint); + if (addOverallDetails) { + addOverallDetailsToRow(timePoint, competitor, row); + } + result.competitors.add(competitorDTO); + Map> futuresForColumnName = new HashMap>(); + final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + for (final RaceColumn raceColumn : this.getRaceColumns()) { + RunnableFuture future = new FutureTask(() -> { + Entry entry = AbstractSimpleLeaderboardImpl.this.getEntry(competitor, raceColumn, timePoint, discardedRaceColumns); + return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint, + namesOfRaceColumnsForWhichToLoadLegDetails != null + && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn + .getName()), waitForLatestAnalyses, legRanksCache, baseDomainFactory, + fillNetPointsUncorrected, cache); + }); + executor.execute(future); + futuresForColumnName.put(raceColumn.getName(), future); + } + for (Map.Entry> raceColumnNameAndFuture : futuresForColumnName.entrySet()) { + try { + row.fieldsByRaceColumnName.put(raceColumnNameAndFuture.getKey(), raceColumnNameAndFuture.getValue().get()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept + // null values. + // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So + // this helps robustness from a user's perspective. + logger.log( + Level.SEVERE, + AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " + + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails + + "): exception during computing leaderboard entry for competitor " + + competitor.getName() + " in race column " + raceColumnNameAndFuture.getKey() + + ". Leaving empty.", e); + } + } + result.rows.put(competitorDTO, row); + String displayName = this.getDisplayName(competitor); + if (displayName != null) { + result.competitorDisplayNames.put(competitorDTO, displayName); + } + } + logger.info("computeLeaderboardByName(" + this.getName() + ", " + timePoint + ", " + + namesOfRaceColumnsForWhichToLoadLegDetails + ", addOverallDetails=" + addOverallDetails + ") took " + + (System.currentTimeMillis() - startOfRequestHandling) + "ms"); + return result; + } + + private void calculateRacesMetadata(MetaLeaderboardColumn metaLeaderboardColumn, MetaLeaderboardRaceColumnDTO columnDTO, + TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory) { + for (final RaceColumn raceColumn : metaLeaderboardColumn.getLeaderboard().getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + String regattaName = trackedRace.getTrackedRegatta().getRegatta().getName(); + String raceName = trackedRace.getRace().getName(); + RegattaAndRaceIdentifier raceIdentifier = new RegattaNameAndRaceName(regattaName, raceName); + columnDTO.addRace(baseDomainFactory.createRaceDTO(trackedRegattaRegistry, + /* withGeoLocationData */ false, raceIdentifier, trackedRace)); + } + } + } + } + + private void addOverallDetailsToRow(final TimePoint timePoint, + final Competitor competitor, LeaderboardRowDTO row) throws NoWindException { + final com.sap.sse.common.Util.Pair maximumSpeedOverGround = this.getMaximumSpeedOverGround(competitor, timePoint); + if (maximumSpeedOverGround != null && maximumSpeedOverGround.getB() != null) { + row.maximumSpeedOverGroundInKnots = maximumSpeedOverGround.getB().getKnots(); + row.whenMaximumSpeedOverGroundWasAchieved = maximumSpeedOverGround.getA().getTimePoint().asDate(); + } + Map legTypeCache = new HashMap<>(); + final Duration totalTimeSailedDownwind = this.getTotalTimeSailedInLegType(competitor, LegType.DOWNWIND, timePoint, legTypeCache); + row.totalTimeSailedDownwindInSeconds = totalTimeSailedDownwind==null?null:totalTimeSailedDownwind.asSeconds(); + final Duration totalTimeSailedUpwind = this.getTotalTimeSailedInLegType(competitor, LegType.UPWIND, timePoint, legTypeCache); + row.totalTimeSailedUpwindInSeconds = totalTimeSailedUpwind==null?null:totalTimeSailedUpwind.asSeconds(); + final Duration totalTimeSailedReaching = this.getTotalTimeSailedInLegType(competitor, LegType.REACHING, timePoint, legTypeCache); + row.totalTimeSailedReachingInSeconds = totalTimeSailedReaching==null?null:totalTimeSailedReaching.asSeconds(); + final Duration totalTimeSailed = this.getTotalTimeSailed(competitor, timePoint); + row.totalTimeSailedInSeconds = totalTimeSailed==null?null:totalTimeSailed.asSeconds(); + final Distance totalDistanceTraveledInMeters = this.getTotalDistanceTraveled(competitor, timePoint); + row.totalDistanceTraveledInMeters = totalDistanceTraveledInMeters==null?null:totalDistanceTraveledInMeters.getMeters(); + } + + /** + * @param waitForLatestAnalyses + * if false, this method is allowed to read the maneuver analysis results from a cache that + * may not reflect all data already received; otherwise, the method will always block for the latest + * cache updates to have happened before returning. + * @param fillNetPointsUncorrected + * tells if {@link LeaderboardEntryDTO#netPointsUncorrected} shall be filled; filling it is rather + * expensive, especially when compared to simply retrieving a score correction, and particularly if in a + * larger fleet a number of competitors haven't properly finished the race. This should only be used for + * leaderboard editing where a user needs to see what the uncorrected score was that would be used when + * the correction was removed. + */ + private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, RaceColumn raceColumn, Competitor competitor, + TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses, + Map> legRanksCache, DomainFactory baseDomainFactory, + boolean fillNetPointsUncorrected, WindLegTypeAndLegBearingCache cache) { + LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); + TrackedRace trackedRace = raceColumn.getTrackedRace(competitor); + entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); + entryDTO.netPoints = entry.getNetPoints(); + if (fillNetPointsUncorrected) { + entryDTO.netPointsUncorrected = entry.getNetPointsUncorrected(); + } + entryDTO.netPointsCorrected = entry.isNetPointsCorrected(); + entryDTO.totalPoints = entry.getTotalPoints(); + entryDTO.reasonForMaxPoints = entry.getMaxPointsReason(); + entryDTO.discarded = entry.isDiscarded(); + final GPSFixTrack track = trackedRace == null ? null : trackedRace.getTrack(competitor); + if (trackedRace != null) { + Date timePointOfLastPositionFixAtOrBeforeQueryTimePoint = getTimePointOfLastFixAtOrBefore(competitor, trackedRace, timePoint); + if (track != null) { + entryDTO.averageSamplingInterval = track.getAverageIntervalBetweenRawFixes(); + } + if (timePointOfLastPositionFixAtOrBeforeQueryTimePoint != null) { + long timeDifferenceInMs = timePoint.asMillis() - timePointOfLastPositionFixAtOrBeforeQueryTimePoint.getTime(); + entryDTO.timeSinceLastPositionFixInSeconds = timeDifferenceInMs == 0 ? 0.0 : timeDifferenceInMs / 1000.0; + } else { + entryDTO.timeSinceLastPositionFixInSeconds = null; + } + } + if (addLegDetails && trackedRace != null) { + try { + final RankingInfo rankingInfo = trackedRace.getRankingMetric().getRankingInfo(timePoint, cache); + RaceDetails raceDetails = getRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, + legRanksCache, rankingInfo, cache); + entryDTO.legDetails = raceDetails.getLegDetails(); + entryDTO.windwardDistanceToCompetitorFarthestAheadInMeters = raceDetails.getWindwardDistanceToCompetitorFarthestAhead() == null ? null + : raceDetails.getWindwardDistanceToCompetitorFarthestAhead().getMeters(); + entryDTO.gapToLeaderInOwnTime = trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache); + entryDTO.averageAbsoluteCrossTrackErrorInMeters = raceDetails.getAverageAbsoluteCrossTrackError() == null ? null + : raceDetails.getAverageAbsoluteCrossTrackError().getMeters(); + entryDTO.averageSignedCrossTrackErrorInMeters = raceDetails.getAverageSignedCrossTrackError() == null ? null + : raceDetails.getAverageSignedCrossTrackError().getMeters(); + entryDTO.calculatedTime = raceDetails.getCorrectedTime(); + entryDTO.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = raceDetails.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead(); + entryDTO.gapToLeaderInOwnTime = raceDetails.getGapToLeaderInOwnTime(); + final TimePoint startOfRace = trackedRace.getStartOfRace(); + if (startOfRace != null) { + Waypoint startWaypoint = trackedRace.getRace().getCourse().getFirstWaypoint(); + NavigableSet competitorMarkPassings = trackedRace.getMarkPassings(competitor); + trackedRace.lockForRead(competitorMarkPassings); + try { + if (!competitorMarkPassings.isEmpty()) { + final MarkPassing firstMarkPassing = competitorMarkPassings.iterator().next(); + if (firstMarkPassing.getWaypoint() == startWaypoint) { + Distance distanceToStartLineFiveSecondsBeforeStartOfRace = trackedRace.getDistanceToStartLine(competitor, /*milliseconds before start*/ 5000); + entryDTO.distanceToStartLineFiveSecondsBeforeStartInMeters = distanceToStartLineFiveSecondsBeforeStartOfRace == null ? null + : distanceToStartLineFiveSecondsBeforeStartOfRace.getMeters(); + Speed speedFiveSecondsBeforeStartOfRace = trackedRace.getSpeed(competitor, /*milliseconds before start*/ 5000); + entryDTO.speedOverGroundFiveSecondsBeforeStartInKnots = speedFiveSecondsBeforeStartOfRace == null ? null + : speedFiveSecondsBeforeStartOfRace.getKnots(); + Distance distanceToStartLineAtStartOfRace = trackedRace.getDistanceToStartLine( + competitor, startOfRace); + entryDTO.distanceToStartLineAtStartOfRaceInMeters = distanceToStartLineAtStartOfRace == null ? null + : distanceToStartLineAtStartOfRace.getMeters(); + Speed speedAtStartTime = track == null ? null : track.getEstimatedSpeed(startOfRace); + entryDTO.speedOverGroundAtStartOfRaceInKnots = speedAtStartTime == null ? null + : speedAtStartTime.getKnots(); + TimePoint competitorStartTime = firstMarkPassing.getTimePoint(); + entryDTO.timeBetweenRaceStartAndCompetitorStartInSeconds = startOfRace.until(competitorStartTime).asSeconds(); + Speed competitorSpeedWhenPassingStart = track == null ? null : track + .getEstimatedSpeed(competitorStartTime); + entryDTO.speedOverGroundAtPassingStartWaypointInKnots = competitorSpeedWhenPassingStart == null ? null + : competitorSpeedWhenPassingStart.getKnots(); + try { + entryDTO.startTack = trackedRace.getTack(competitor, competitorStartTime); + } catch (NoWindException nwe) { + entryDTO.startTack = null; // leave empty in case no wind information is available + } + Distance distanceFromStarboardSideOfStartLineWhenPassingStart = trackedRace + .getDistanceFromStarboardSideOfStartLineWhenPassingStart(competitor); + entryDTO.distanceToStarboardSideOfStartLineInMeters = distanceFromStarboardSideOfStartLineWhenPassingStart == null ? null + : distanceFromStarboardSideOfStartLineWhenPassingStart.getMeters(); + } + } + } finally { + trackedRace.unlockAfterRead(competitorMarkPassings); + } + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); // the future used to calculate the leg details was interrupted; escalate as runtime exception + } + } + final Fleet fleet = entry.getFleet(); + entryDTO.fleet = fleet == null ? null : baseDomainFactory.convertToFleetDTO(fleet); + return entryDTO; + } + + /** + * Determines the time point of the last raw fix (with outliers not removed) for competitor in + * trackedRace. If the competitor's track is null or empty, null is returned. + * @param trackedRace must not be null + * @param atOrBefore find the last fix at or before the time point specified + */ + private Date getTimePointOfLastFixAtOrBefore(Competitor competitor, TrackedRace trackedRace, TimePoint atOrBefore) { + assert trackedRace != null; + final Date timePointOfLastPositionFix; + GPSFixTrack track = trackedRace.getTrack(competitor); + if (track == null) { + timePointOfLastPositionFix = null; + } else { + GPSFixMoving lastFix = track.getLastFixAtOrBefore(atOrBefore); + if (lastFix == null) { + timePointOfLastPositionFix = null; + } else { + timePointOfLastPositionFix = lastFix.getTimePoint().asDate(); + } + } + return timePointOfLastPositionFix; + } + + private static class RaceDetails { + private final List legDetails; + private final Distance windwardDistanceToCompetitorFarthestAhead; + private final Distance averageAbsoluteCrossTrackError; + private final Distance averageSignedCrossTrackError; + private final Duration gapToLeaderInOwnTime; + private final Duration correctedTime; + private final Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + + public RaceDetails(List legDetails, Distance windwardDistanceToCompetitorFarthestAhead, + Distance averageAbsoluteCrossTrackError, Distance averageSignedCrossTrackError, + Duration gapToLeaderInOwnTime, Duration correctedTime, + Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { + super(); + this.legDetails = legDetails; + this.windwardDistanceToCompetitorFarthestAhead = windwardDistanceToCompetitorFarthestAhead; + this.averageAbsoluteCrossTrackError = averageAbsoluteCrossTrackError; + this.averageSignedCrossTrackError = averageSignedCrossTrackError; + this.gapToLeaderInOwnTime = gapToLeaderInOwnTime; + this.correctedTime = correctedTime; + this.correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + } + public List getLegDetails() { + return legDetails; + } + public Distance getWindwardDistanceToCompetitorFarthestAhead() { + return windwardDistanceToCompetitorFarthestAhead; + } + public Distance getAverageAbsoluteCrossTrackError() { + return averageAbsoluteCrossTrackError; + } + public Distance getAverageSignedCrossTrackError() { + return averageSignedCrossTrackError; + } + public Duration getGapToLeaderInOwnTime() { + return gapToLeaderInOwnTime; + } + public Duration getCorrectedTime() { + return correctedTime; + } + public Duration getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead() { + return correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + } + } + + /** + * If timePoint is after the end of the race's tracking the query will be adjusted to obtain the values + * at the end of the {@link TrackedRace#getEndOfTracking() race's tracking time}. If the time point adjusted this + * way equals the end of the tracking time, the query results will be looked up in a cache first and if not found, + * they will be stored to the cache after calculating them. A cache invalidation {@link RaceChangeListener listener} + * will be registered with the race which will be triggered for any event received by the race. + * @param waitForLatestAnalyses + * if false, this method is allowed to read the maneuver analysis results from a cache that + * may not reflect all data already received; otherwise, the method will always block for the latest + * cache updates to have happened before returning. + */ + private RaceDetails getRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { + final RaceDetails raceDetails; + if (trackedRace.getEndOfTracking() != null && trackedRace.getEndOfTracking().compareTo(timePoint) < 0) { + raceDetails = getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(trackedRace, competitor, legRanksCache, rankingInfo, cache); + } else { + raceDetails = calculateRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache, cache, rankingInfo); + } + return raceDetails; + } + + private RaceDetails getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(final TrackedRace trackedRace, + final Competitor competitor, final Map> legRanksCache, + RankingInfo rankingInfo, final WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { + final com.sap.sse.common.Util.Pair key = new com.sap.sse.common.Util.Pair(trackedRace, competitor); + RunnableFuture raceDetails; + synchronized (raceDetailsAtEndOfTrackingCache) { + raceDetails = raceDetailsAtEndOfTrackingCache.get(key); + if (raceDetails == null) { + raceDetails = new FutureTask(new Callable() { + @Override + public RaceDetails call() throws Exception { + TimePoint end = trackedRace.getEndOfRace(); + if (end == null) { + end = trackedRace.getEndOfTracking(); + } + return calculateRaceDetails(trackedRace, competitor, end, + // TODO see bug 1358: for now, use waitForLatest==false until we've switched to optimistic locking for the course read lock + /* TODO old comment when it was still true: "because this is done only once after end of tracking" */ + /* waitForLatestAnalyses (maneuver and cross track error) */ false, + legRanksCache, cache, rankingInfo); + } + }); + raceDetailsExecutor.execute(raceDetails); + raceDetailsAtEndOfTrackingCache.put(key, raceDetails); + final CacheInvalidationListener cacheInvalidationListener = new CacheInvalidationListener(trackedRace, competitor); + trackedRace.addListener(cacheInvalidationListener); + cacheInvalidationListeners.add(cacheInvalidationListener); + } + } + return raceDetails.get(); + } + + private RaceDetails calculateRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + WindLegTypeAndLegBearingCache cache, final RankingInfo rankingInfo) { + final List legDetails = new ArrayList(); + final Course course = trackedRace.getRace().getCourse(); + course.lockForRead(); // hold back any course re-configurations while looping over the legs + try { + for (Leg leg : course.getLegs()) { + LegEntryDTO legEntry; + // We loop over a copy of the course's legs; during a course change, legs may become "stale," even with + // regard to the leg/trackedLeg structures inside the tracked race which is updated by the course change + // immediately. That's why we've acquired a read lock for the course above. + TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, leg); + if (trackedLeg != null && trackedLeg.hasStartedLeg(timePoint)) { + legEntry = createLegEntry(trackedLeg, timePoint, waitForLatestAnalyses, legRanksCache, rankingInfo, cache); + } else { + legEntry = null; + } + legDetails.add(legEntry); + } + final Distance windwardDistanceToCompetitorFarthestAhead = trackedRace == null ? null : trackedRace + .getWindwardDistanceToCompetitorFarthestAhead(competitor, timePoint, WindPositionMode.LEG_MIDDLE, rankingInfo, cache); + Distance averageAbsoluteCrossTrackError; + try { + averageAbsoluteCrossTrackError = trackedRace == null ? null : trackedRace.getAverageAbsoluteCrossTrackError( + competitor, timePoint, waitForLatestAnalyses, cache); + } catch (NoWindException nwe) { + // without wind information, use null meaning "unknown" + averageAbsoluteCrossTrackError = null; + } + Distance averageSignedCrossTrackError; + try { + averageSignedCrossTrackError = trackedRace == null ? null : trackedRace.getAverageSignedCrossTrackError( + competitor, timePoint, waitForLatestAnalyses, cache); + } catch (NoWindException nwe) { + // without wind information, use null meaning "unknown" + averageSignedCrossTrackError = null; + } + final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); + return new RaceDetails(legDetails, windwardDistanceToCompetitorFarthestAhead, averageAbsoluteCrossTrackError, averageSignedCrossTrackError, + trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache), + trackedRace.getRankingMetric().getCorrectedTime(competitor, timePoint), + competitorRankingInfo == null ? null : competitorRankingInfo.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead()); + } finally { + course.unlockAfterRead(); + } + } + + private LegEntryDTO createLegEntry(TrackedLegOfCompetitor trackedLeg, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { + LegEntryDTO result; + final Duration time = trackedLeg.getTime(timePoint); + if (trackedLeg == null || time == null) { + result = null; + } else { + result = new LegEntryDTO(); + try { + result.legType = trackedLeg.getTrackedLeg().getLegType(timePoint); + } catch (NoWindException nwe) { + result.legType = null; // can't determine leg type without wind data + } + final Speed averageSpeedOverGround = trackedLeg.getAverageSpeedOverGround(timePoint); + result.averageSpeedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); + Distance averageAbsoluteCrossTrackError; + try { + averageAbsoluteCrossTrackError = trackedLeg.getAverageAbsoluteCrossTrackError(timePoint, waitForLatestAnalyses); + } catch (NoWindException nwe) { + // leave averageAbsoluteCrossTrackError as null, meaning "unknown" + averageAbsoluteCrossTrackError = null; + } + result.averageAbsoluteCrossTrackErrorInMeters = averageAbsoluteCrossTrackError == null ? null : averageAbsoluteCrossTrackError.getMeters(); + Distance averageSignedCrossTrackError; + try { + averageSignedCrossTrackError = trackedLeg.getAverageSignedCrossTrackError(timePoint, waitForLatestAnalyses); + } catch (NoWindException nwe) { + // leave averageSignedCrossTrackError as null, meaning "unknown" + averageSignedCrossTrackError = null; + } + result.averageSignedCrossTrackErrorInMeters = averageSignedCrossTrackError == null ? null : averageSignedCrossTrackError.getMeters(); + Double speedOverGroundInKnots; + if (trackedLeg.hasFinishedLeg(timePoint)) { + speedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); + } else { + final SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); + speedOverGroundInKnots = speedOverGround == null ? null : speedOverGround.getKnots(); + } + result.currentSpeedOverGroundInKnots = speedOverGroundInKnots == null ? null : speedOverGroundInKnots; + Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint); + result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters(); + Distance distanceTraveledConsideringGateStart = trackedLeg.getDistanceTraveledConsideringGateStart(timePoint); + result.distanceTraveledIncludingGateStartInMeters = distanceTraveledConsideringGateStart == null ? null : distanceTraveledConsideringGateStart.getMeters(); + final Duration estimatedTimeToNextMarkInSeconds = trackedLeg.getEstimatedTimeToNextMark(timePoint, WindPositionMode.EXACT, cache); + result.estimatedTimeToNextWaypointInSeconds = estimatedTimeToNextMarkInSeconds==null?null:estimatedTimeToNextMarkInSeconds.asSeconds(); + result.timeInMilliseconds = time.asMillis(); + result.finished = trackedLeg.hasFinishedLeg(timePoint); + final TimePoint legFinishTime = trackedLeg.getFinishTime(); + result.correctedTotalTime = trackedLeg.hasStartedLeg(timePoint) ? trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getCorrectedTime(trackedLeg.getCompetitor(), + trackedLeg.hasFinishedLeg(timePoint) ? legFinishTime : timePoint, cache) : null; + // fetch the leg gap in own corrected time from the ranking metric + final Duration gapToLeaderInOwnTime = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric(). + getLegGapToLegLeaderInOwnTime(trackedLeg, timePoint, rankingInfo, cache); + result.gapToLeaderInSeconds = gapToLeaderInOwnTime == null ? null : gapToLeaderInOwnTime.asSeconds(); + if (result.gapToLeaderInSeconds != null) { + final Duration gapAtEndOfPreviousLeg = getGapAtEndOfPreviousLeg(trackedLeg, rankingInfo, cache); + if (gapAtEndOfPreviousLeg != null) { + result.gapChangeSinceLegStartInSeconds = result.gapToLeaderInSeconds - gapAtEndOfPreviousLeg.asSeconds(); + } + } + LinkedHashMap legRanks = legRanksCache.get(trackedLeg.getLeg()); + if (legRanks != null) { + result.rank = legRanks.get(trackedLeg.getCompetitor()); + } else { + result.rank = trackedLeg.getRank(timePoint, cache); + } + result.started = trackedLeg.hasStartedLeg(timePoint); + Speed velocityMadeGood; + if (trackedLeg.hasFinishedLeg(timePoint)) { + velocityMadeGood = trackedLeg.getAverageVelocityMadeGood(timePoint); + } else { + velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint, WindPositionMode.EXACT); + } + result.velocityMadeGoodInKnots = velocityMadeGood == null ? null : velocityMadeGood.getKnots(); + Distance windwardDistanceToGo = trackedLeg.getWindwardDistanceToGo(timePoint, WindPositionMode.LEG_MIDDLE); + result.windwardDistanceToGoInMeters = windwardDistanceToGo == null ? null : windwardDistanceToGo + .getMeters(); + final TimePoint startOfRace = trackedLeg.getTrackedLeg().getTrackedRace().getStartOfRace(); + if (startOfRace != null && trackedLeg.hasStartedLeg(timePoint)) { + // not using trackedLeg.getManeuvers(...) because it may not catch the mark passing maneuver starting this leg + // because that may have been detected as slightly before the mark passing time, hence associated with the previous leg + List maneuvers = trackedLeg.getTrackedLeg().getTrackedRace() + .getManeuvers(trackedLeg.getCompetitor(), startOfRace, timePoint, waitForLatestAnalyses); + if (maneuvers != null) { + result.numberOfManeuvers = new HashMap(); + result.numberOfManeuvers.put(ManeuverType.TACK, 0); + result.numberOfManeuvers.put(ManeuverType.JIBE, 0); + result.numberOfManeuvers.put(ManeuverType.PENALTY_CIRCLE, 0); + Map totalManeuverLossInMeters = new HashMap(); + totalManeuverLossInMeters.put(ManeuverType.TACK, 0.0); + totalManeuverLossInMeters.put(ManeuverType.JIBE, 0.0); + totalManeuverLossInMeters.put(ManeuverType.PENALTY_CIRCLE, 0.0); + TimePoint startOfLeg = trackedLeg.getStartTime(); + for (Maneuver maneuver : maneuvers) { + // don't count maneuvers that were in previous legs + switch (maneuver.getType()) { + case TACK: + case JIBE: + case PENALTY_CIRCLE: + if (!maneuver.getTimePoint().before(startOfLeg) && (legFinishTime == null || legFinishTime.after(timePoint) || + maneuver.getTimePoint().before(legFinishTime))) { + if (maneuver.getManeuverLoss() != null) { + result.numberOfManeuvers.put(maneuver.getType(), + result.numberOfManeuvers.get(maneuver.getType()) + 1); + totalManeuverLossInMeters.put(maneuver.getType(), + totalManeuverLossInMeters.get(maneuver.getType()) + + maneuver.getManeuverLoss().getMeters()); + } + } + break; + case MARK_PASSING: + // analyze all mark passings, not only those after this leg's start, to catch the mark passing + // maneuver starting this leg, even if its time point is slightly before the mark passing starting this leg + MarkPassingManeuver mpm = (MarkPassingManeuver) maneuver; + if (mpm.getWaypointPassed() == trackedLeg.getLeg().getFrom()) { + result.sideToWhichMarkAtLegStartWasRounded = mpm.getSide(); + } + break; + default: + /* Do nothing here. + * Throwing an exception destroys the toggling (and maybe other behaviour) of the leaderboard. + */ + } + } + result.averageManeuverLossInMeters = new HashMap(); + for (ManeuverType maneuverType : new ManeuverType[] { ManeuverType.TACK, ManeuverType.JIBE, + ManeuverType.PENALTY_CIRCLE }) { + if (result.numberOfManeuvers.get(maneuverType) != 0) { + result.averageManeuverLossInMeters.put( + maneuverType, + totalManeuverLossInMeters.get(maneuverType) + / result.numberOfManeuvers.get(maneuverType)); + } + } + } + } + } + return result; + } + + private Duration getGapAtEndOfPreviousLeg(TrackedLegOfCompetitor trackedLeg, final RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { + final Duration result; + final Course course = trackedLeg.getTrackedLeg().getTrackedRace().getRace().getCourse(); + // if trackedLeg is the first leg, compute the gap at the start of this leg; otherwise, compute gap + // at the end of the previous leg + final TimePoint timePoint = trackedLeg.getStartTime(); + final TrackedLegOfCompetitor tloc; + if (course.getFirstWaypoint() == trackedLeg.getLeg().getFrom()) { + tloc = trackedLeg; + } else { + tloc = trackedLeg.getTrackedLeg().getTrackedRace().getTrackedLegFinishingAt(trackedLeg.getLeg().getFrom()) + .getTrackedLeg(trackedLeg.getCompetitor()); + } + result = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getLegGapToLegLeaderInOwnTime(tloc, timePoint, rankingInfo, cache); + return result; + } + + private LeaderboardDTO getLiveLeaderboard(Collection namesOfRaceColumnsForWhichToLoadLegDetails, + boolean addOverallDetails, TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, ExecutionException { + LiveLeaderboardUpdater liveLeaderboardUpdater = getLiveLeaderboardUpdater(trackedRegattaRegistry, + baseDomainFactory); + return liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); + } + + private LiveLeaderboardUpdater getLiveLeaderboardUpdater(TrackedRegattaRegistry trackedRegattaRegistry, + DomainFactory baseDomainFactory) { + LiveLeaderboardUpdater result = this.liveLeaderboardUpdater; + if (result == null) { + synchronized (this) { + result = this.liveLeaderboardUpdater; + if (result == null) { + this.liveLeaderboardUpdater = new LiveLeaderboardUpdater(this, trackedRegattaRegistry, baseDomainFactory); + result = this.liveLeaderboardUpdater; + } + } + } + return result; + } + + private LeaderboardDTOCache getLeaderboardDTOCache() { + LeaderboardDTOCache result = this.leaderboardDTOCache; + if (result == null) { + synchronized (this) { + result = this.leaderboardDTOCache; + if (result == null) { + // The leaderboard cache is invalidated upon all competitor and mark position changes; some analyzes + // are pretty expensive, such as the maneuver re-calculation. Waiting for the latest analysis after only a + // single fix was updated is too expensive if users use the replay feature while a race is still running. + // Therefore, using waitForLatestAnalyses==false seems appropriate here. + this.leaderboardDTOCache = new LeaderboardDTOCache(/* waitForLatestAnalyses */false, this); + result = this.leaderboardDTOCache; + } + } + } + return result; + } + + @Override + public LeaderboardDTO getLeaderboardDTO(TimePoint timePoint, + Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory, boolean fillNetPointsUncorrected) throws NoWindException, + InterruptedException, ExecutionException { + LeaderboardDTO result = null; + if (timePoint == null) { + // timePoint==null means live mode; however, if we're after the end of all races and after all score + // corrections, don't use the live leaderboard updater which would keep re-calculating over and over again, but map + // this to a usual non-live call which uses the regular LeaderboardDTOCache which is invalidated properly + // when the tracked race associations or score corrections or tracked race contents changes: + final TimePoint nowMinusDelay = this.getNowMinusDelay(); + final TimePoint timePointOfLatestModification = this.getTimePointOfLatestModification(); + if (fillNetPointsUncorrected || (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification))) { + // if there hasn't been any modification to the leaderboard since nowMinusDelay, use non-live mode + // and pull the result from the regular leaderboard cache: + timePoint = timePointOfLatestModification; + } else { + // don't use the regular leaderboard cache; the race still seems to be on; use the live leaderboard updater instead: + timePoint = null; + result = this.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, trackedRegattaRegistry, baseDomainFactory); + } + } + if (timePoint != null) { + if (fillNetPointsUncorrected) { + // explicitly filling the uncorrected net points requires uncached recalculation + result = computeDTO(timePoint, namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, /* waitForLatestAnalyses */ true, + trackedRegattaRegistry, baseDomainFactory, fillNetPointsUncorrected); + } else { + // in replay we'd like up-to-date results; they are still cached + // which is OK because the cache is invalidated whenever any of the tracked races attached to the + // leaderboard changes. + result = getLeaderboardDTOCache().getLeaderboardByName(timePoint, + namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, baseDomainFactory, + trackedRegattaRegistry); + } + } + return result; + } + + public String toString() { + return getName() + " " + (getDefaultCourseArea() != null ? getDefaultCourseArea().getName() : "") + " " + (getScoringScheme() != null ? getScoringScheme().getType().name() : ""); + } + + @Override + public NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher() { + return new NumberOfCompetitorsFetcherImpl(); + } +} diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java index 38860207a43..a6123d06096 100755 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java @@ -1,162 +1,162 @@ -package com.sap.sailing.gwt.ui.test; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import org.junit.Before; -import org.junit.Test; - -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.gwt.ui.server.SailingServiceImpl; - -public class TestColumnSwapping { - - private LeaderboardDTO lb = null; - private static final String LEADERBOARDNAME = "test"; - private static final String DEFAULT_FLEET_NAME = "Default"; - private static final FleetDTO DEFAULT_FLEET = new FleetDTO(DEFAULT_FLEET_NAME, /* ordering */ 0, /* color */ null); - private SailingServiceImpl service; - private LeaderboardDTO leaderboardOriginalDTO; - private LeaderboardDTO leaderboardDTO; - private Collection leglist; - private Date leaderboardCreationDate; - private SailingServiceImpl sailingService = null; - - // Test-Data - private final String TEST_LEADERBOARD_NAME = "test_board"; - private final String[] races = { "1", "2", "3" }; - private final boolean[] isMedalRace = { false, false, true }; - - @Before - public void prepareColumnSwapping() { - service = new SailingServiceImplMock(); - int[] disc = { 5, 8, 9, 0, 7, 5, 43 }; - service.removeLeaderboard(LEADERBOARDNAME); - service.createFlexibleLeaderboard(LEADERBOARDNAME, null, disc, ScoringSchemeType.LOW_POINT, null); - service.addColumnToLeaderboard("Race1", LEADERBOARDNAME, true); - service.addColumnToLeaderboard("Race2", LEADERBOARDNAME, true); - service.addColumnToLeaderboard("Race3", LEADERBOARDNAME, true); - leglist = new ArrayList(); - leglist.add("Race1"); - leglist.add("Race2"); - leglist.add("Race3"); - leaderboardCreationDate = new Date(); - try { - // get Leaderboard with name and current date - leaderboardOriginalDTO = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { - @Override - public String generateRandomUUID() { - return UUID.randomUUID().toString(); - } - }, /* hasOverallDetails */ false); - leaderboardOriginalDTO.addRace("Race1", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - leaderboardOriginalDTO.addRace("Race3", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - leaderboardOriginalDTO.addRace("Race2", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Test - public void testColumnSwapping() { - testColumnSwappingFabian(); - testLeaderBoardDTOMethods(); - testSailingService(); - } - - @Test - public void testSailingService() { - sailingService = new SailingServiceImplMock(); - assertNotNull("Sailingservice != NULL", sailingService); - int td[] = { 5, 8 }; - sailingService.removeLeaderboard(TEST_LEADERBOARD_NAME); - sailingService.createFlexibleLeaderboard(TEST_LEADERBOARD_NAME, null, td, ScoringSchemeType.LOW_POINT, null); - for (int i = 0; i < races.length; i++) - sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - sailingService.moveLeaderboardColumnUp(TEST_LEADERBOARD_NAME, races[2]); - sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true); - sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false); - try { - for (int i = 0; i < races.length; i++) - sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - sailingService.moveLeaderboardColumnUp(TEST_LEADERBOARD_NAME, races[2]); - sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true); - sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false); - lb = sailingService.getLeaderboardByName(TEST_LEADERBOARD_NAME, new Date(), /* races to load */ null, /* addOverallDetails */ true, /* previous leaderboard ID */ null, - /* fillNetPointsUncorrected */ false).getLeaderboardDTO(/* previousVersion */ null); - } catch (Exception e) { - // e.printStackTrace(); - fail(e.getLocalizedMessage()); - } - assertNotNull("LeaderboardDTO != NULL", lb); - - // asserted data - String[] raceNames = new String[] { "3", "2", "1" }; - boolean[] medalRace = { true, false, false }; - - for (int i = 0; i < lb.getRaceList().size(); i++) { - assertEquals("Race[" + i + "] == " + raceNames[i], raceNames[i], lb.getRaceList().get(i).getName()); - assertTrue("Race[" + i + "] is " + (medalRace[i] ? "" : "no ") + "medalrace.", - lb.raceIsMedalRace(races[i]) == medalRace[i]); - } - } - - @Test - public void testLeaderBoardDTOMethods() { - lb = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { - @Override - public String generateRandomUUID() { - return UUID.randomUUID().toString(); - } - }, /* hasOverallDetails */ false); - assertNotNull("Leaderboard != NULL", lb); - lb.addRace("1", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null); - lb.addRace("2", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null); - lb.addRace("3", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - lb.moveRaceDown("1"); - String[] s = new String[] { "2", "1", "3" }; - for (int i = 0; i < lb.getRaceList().size(); i++) { - assertEquals("Race[" + i + "] = " + s[i], s[i], lb.getRaceList().get(i).getName()); - } - } - - @Test - public void testColumnSwappingFabian() { - service.moveLeaderboardColumnUp(LEADERBOARDNAME, "Race3"); - try { - leaderboardDTO = service.getLeaderboardByName(LEADERBOARDNAME, leaderboardCreationDate, leglist, /* addOverallDetails */ - true, /* previous leaderboard ID */null, /* fillNetPointsUncorrected */false).getLeaderboardDTO(/* previousVersion */null); - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - // check if leaderboardDTO an dleaderboardOriginalDTO same - List leaderboardList = leaderboardDTO.getRaceList(); - List leaderboardOriginalList = leaderboardOriginalDTO.getRaceList(); - - // ??????? assert races in list - assertArrayEquals(leaderboardList.toArray(), leaderboardOriginalList.toArray()); - - for (RaceColumnDTO raceDTO : leaderboardOriginalList) { - assert leaderboardDTO.raceIsMedalRace(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsMedalRace(raceDTO.getRaceColumnName()); - assert leaderboardDTO.raceIsTracked(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsTracked(raceDTO.getRaceColumnName()); - } - } -} +package com.sap.sailing.gwt.ui.test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.gwt.ui.server.SailingServiceImpl; + +public class TestColumnSwapping { + + private LeaderboardDTO lb = null; + private static final String LEADERBOARDNAME = "test"; + private static final String DEFAULT_FLEET_NAME = "Default"; + private static final FleetDTO DEFAULT_FLEET = new FleetDTO(DEFAULT_FLEET_NAME, /* ordering */ 0, /* color */ null); + private SailingServiceImpl service; + private LeaderboardDTO leaderboardOriginalDTO; + private LeaderboardDTO leaderboardDTO; + private Collection leglist; + private Date leaderboardCreationDate; + private SailingServiceImpl sailingService = null; + + // Test-Data + private final String TEST_LEADERBOARD_NAME = "test_board"; + private final String[] races = { "1", "2", "3" }; + private final boolean[] isMedalRace = { false, false, true }; + + @Before + public void prepareColumnSwapping() { + service = new SailingServiceImplMock(); + int[] disc = { 5, 8, 9, 0, 7, 5, 43 }; + service.removeLeaderboard(LEADERBOARDNAME); + service.createFlexibleLeaderboard(LEADERBOARDNAME, null, disc, ScoringSchemeType.LOW_POINT, null); + service.addColumnToLeaderboard("Race1", LEADERBOARDNAME, true); + service.addColumnToLeaderboard("Race2", LEADERBOARDNAME, true); + service.addColumnToLeaderboard("Race3", LEADERBOARDNAME, true); + leglist = new ArrayList(); + leglist.add("Race1"); + leglist.add("Race2"); + leglist.add("Race3"); + leaderboardCreationDate = new Date(); + try { + // get Leaderboard with name and current date + leaderboardOriginalDTO = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { + @Override + public String generateRandomUUID() { + return UUID.randomUUID().toString(); + } + }, /* hasOverallDetails */ false); + leaderboardOriginalDTO.addRace("Race1", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + leaderboardOriginalDTO.addRace("Race3", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + leaderboardOriginalDTO.addRace("Race2", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void testColumnSwapping() { + testColumnSwappingFabian(); + testLeaderBoardDTOMethods(); + testSailingService(); + } + + @Test + public void testSailingService() { + sailingService = new SailingServiceImplMock(); + assertNotNull("Sailingservice != NULL", sailingService); + int td[] = { 5, 8 }; + sailingService.removeLeaderboard(TEST_LEADERBOARD_NAME); + sailingService.createFlexibleLeaderboard(TEST_LEADERBOARD_NAME, null, td, ScoringSchemeType.LOW_POINT, null); + for (int i = 0; i < races.length; i++) + sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + sailingService.moveLeaderboardColumnUp(TEST_LEADERBOARD_NAME, races[2]); + sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true); + sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false); + try { + for (int i = 0; i < races.length; i++) + sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + sailingService.moveLeaderboardColumnUp(TEST_LEADERBOARD_NAME, races[2]); + sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true); + sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false); + lb = sailingService.getLeaderboardByName(TEST_LEADERBOARD_NAME, new Date(), /* races to load */ null, /* addOverallDetails */ true, /* previous leaderboard ID */ null, + /* fillNetPointsUncorrected */ false).getLeaderboardDTO(/* previousVersion */ null); + } catch (Exception e) { + // e.printStackTrace(); + fail(e.getLocalizedMessage()); + } + assertNotNull("LeaderboardDTO != NULL", lb); + + // asserted data + String[] raceNames = new String[] { "3", "2", "1" }; + boolean[] medalRace = { true, false, false }; + + for (int i = 0; i < lb.getRaceList().size(); i++) { + assertEquals("Race[" + i + "] == " + raceNames[i], raceNames[i], lb.getRaceList().get(i).getName()); + assertTrue("Race[" + i + "] is " + (medalRace[i] ? "" : "no ") + "medalrace.", + lb.raceIsMedalRace(races[i]) == medalRace[i]); + } + } + + @Test + public void testLeaderBoardDTOMethods() { + lb = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { + @Override + public String generateRandomUUID() { + return UUID.randomUUID().toString(); + } + }, /* hasOverallDetails */ false); + assertNotNull("Leaderboard != NULL", lb); + lb.addRace("1", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null, false); + lb.addRace("2", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null, false); + lb.addRace("3", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + lb.moveRaceDown("1"); + String[] s = new String[] { "2", "1", "3" }; + for (int i = 0; i < lb.getRaceList().size(); i++) { + assertEquals("Race[" + i + "] = " + s[i], s[i], lb.getRaceList().get(i).getName()); + } + } + + @Test + public void testColumnSwappingFabian() { + service.moveLeaderboardColumnUp(LEADERBOARDNAME, "Race3"); + try { + leaderboardDTO = service.getLeaderboardByName(LEADERBOARDNAME, leaderboardCreationDate, leglist, /* addOverallDetails */ + true, /* previous leaderboard ID */null, /* fillNetPointsUncorrected */false).getLeaderboardDTO(/* previousVersion */null); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + // check if leaderboardDTO an dleaderboardOriginalDTO same + List leaderboardList = leaderboardDTO.getRaceList(); + List leaderboardOriginalList = leaderboardOriginalDTO.getRaceList(); + + // ??????? assert races in list + assertArrayEquals(leaderboardList.toArray(), leaderboardOriginalList.toArray()); + + for (RaceColumnDTO raceDTO : leaderboardOriginalList) { + assert leaderboardDTO.raceIsMedalRace(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsMedalRace(raceDTO.getRaceColumnName()); + assert leaderboardDTO.raceIsTracked(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsTracked(raceDTO.getRaceColumnName()); + } + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 2b64fab0ef2..79207753af8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -233,6 +233,7 @@ import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; import com.sap.sailing.domain.leaderboard.caching.LiveLeaderboardUpdater; +import com.sap.sailing.domain.leaderboard.meta.MetaLeaderboardColumn; import com.sap.sailing.domain.persistence.DomainObjectFactory; import com.sap.sailing.domain.persistence.MongoObjectFactory; import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; @@ -853,7 +854,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S final RaceColumnDTO raceColumnDTO = columnFactory.createRaceColumnDTO(raceColumn.getName(), raceColumn.isMedalRace(), raceColumn.getExplicitFactor(), raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null); + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, + raceColumn instanceof MetaLeaderboardColumn); raceColumnDTOs.add(raceColumnDTO); } return raceColumnDTOs; @@ -2384,7 +2386,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S RaceColumnDTO raceColumnDTO = leaderboardDTO.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, - fleetDTO, raceColumn.isMedalRace(), raceIdentifier, raceDTO); + fleetDTO, raceColumn.isMedalRace(), raceIdentifier, raceDTO, raceColumn instanceof MetaLeaderboardColumn); RaceLog raceLog = raceColumn.getRaceLog(fleet); RaceLogTrackingState raceLogTrackingState = raceLog == null ? RaceLogTrackingState.NOT_A_RACELOG_TRACKED_RACE : new RaceLogTrackingStateAnalyzer(raceLog).analyze(); From 78656cc0b4604ed7a0510041879e5c508854d624 Mon Sep 17 00:00:00 2001 From: JDann Date: Tue, 1 Dec 2015 12:15:30 +0100 Subject: [PATCH 072/306] Extraced the AdvantageLineAnimator to its own class and made the animation always stop after 1 second, same as the boats --- .../shared/racemap/AdvantageLineAnimator.java | 66 +++++++++++++++++++ .../gwt/ui/client/shared/racemap/RaceMap.java | 53 ++------------- 2 files changed, 70 insertions(+), 49 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java new file mode 100644 index 00000000000..3bcb5c0ee77 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java @@ -0,0 +1,66 @@ +package com.sap.sailing.gwt.ui.client.shared.racemap; + +import com.google.gwt.maps.client.base.LatLng; +import com.google.gwt.maps.client.mvc.MVCArray; +import com.google.gwt.maps.client.overlays.Polyline; +import com.google.gwt.user.client.Timer; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.NauticalMileDistance; +import com.sap.sailing.gwt.ui.shared.GPSFixDTO; + +class AdvantageLineAnimator extends Timer { + private static final double MILLIS_TO_HOURS_FACTOR = 3600000; + + private Polyline advantageLine; + private GPSFixDTO lastBoatFix; + private long lastTime; + private int repetitions = -1; + + public AdvantageLineAnimator(Polyline advantageLine) { + this.advantageLine = advantageLine; + } + + public void setLastFix(GPSFixDTO lastBoatFix) { + this.lastBoatFix = lastBoatFix; + scheduleRepeating(100, 10); + } + + public void scheduleRepeating(int periodMillis, int repetitions) { + this.lastTime = System.currentTimeMillis(); + this.scheduleRepeating(periodMillis); + this.repetitions = repetitions; + } + + @Override + public void run() { + if (repetitions > 0) { + repetitions--; + } else if (repetitions == 0) { + this.cancel(); + return; + } + + if(advantageLine != null) { + long time = System.currentTimeMillis(); + long deltaTime = time - lastTime; + lastTime = time; + + if (lastBoatFix != null) { + Position oldPosition = lastBoatFix.position; + Position newPosition = oldPosition.translateRhumb( + new DegreeBearingImpl(lastBoatFix.speedWithBearing.bearingInDegrees), + new NauticalMileDistance(deltaTime / MILLIS_TO_HOURS_FACTOR * lastBoatFix.speedWithBearing.speedInKnots)); + + final double latDelta = newPosition.getLatDeg() - oldPosition.getLatDeg(); + final double lngDelta = newPosition.getLngDeg() - oldPosition.getLngDeg(); + + final MVCArray path = this.advantageLine.getPath(); + LatLng pos1 = path.pop(); + LatLng pos2 = path.pop(); + path.push(LatLng.newInstance(pos2.getLatitude() + latDelta, pos2.getLongitude() + lngDelta)); + path.push(LatLng.newInstance(pos1.getLatitude() + latDelta, pos1.getLongitude() + lngDelta)); + } + } + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index 5124e2a0d3a..ff3b0dcd736 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 @@ -50,7 +50,6 @@ 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.mvc.MVCArrayCallback; import com.google.gwt.maps.client.overlays.InfoWindow; import com.google.gwt.maps.client.overlays.InfoWindowOptions; import com.google.gwt.maps.client.overlays.Marker; @@ -82,8 +81,6 @@ 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.impl.NauticalMileDistance; 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.GetPolarAction; @@ -177,7 +174,9 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe /** * Polyline for the advantage line (the leading line for the boats, orthogonal to the wind direction; touching the leading boat). */ - private Polyline advantageLine; + Polyline advantageLine; + + private AdvantageLineAnimator advantageTimer; /** * The windward of two Polylines representing a triangle between startline and first mark. @@ -1224,51 +1223,6 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe } return null; } - - private class AdvantageLineAnimator extends com.google.gwt.user.client.Timer { - private static final double MILLIS_TO_HOURS_FACTOR = 3600000; - - private DelegateCoordinateSystem coordinateSystem; - private GPSFixDTO lastBoatFix; - private long lastTime; - - public AdvantageLineAnimator(DelegateCoordinateSystem coordinateSystem) { - this.coordinateSystem = coordinateSystem; - this.scheduleRepeating(100); - this.lastTime = System.currentTimeMillis(); - } - - public void setLastFix(GPSFixDTO lastBoatFix) { - this.lastBoatFix = lastBoatFix; - } - - @Override - public void run() { - long time = System.currentTimeMillis(); - long deltaTime = time - lastTime; - lastTime = time; - - if (lastBoatFix != null) { - Position oldPosition = lastBoatFix.position; - Position newPosition = oldPosition.translateRhumb( - new DegreeBearingImpl(lastBoatFix.speedWithBearing.bearingInDegrees), - new NauticalMileDistance(deltaTime / MILLIS_TO_HOURS_FACTOR * lastBoatFix.speedWithBearing.speedInKnots)); - - final double latDelta = newPosition.getLatDeg() - oldPosition.getLatDeg(); - final double lngDelta = newPosition.getLngDeg() - oldPosition.getLngDeg(); - - final MVCArray path = advantageLine.getPath(); - advantageLine.getPath().forEach(new MVCArrayCallback() { - @Override - public void forEach(LatLng element, int index) { - path.setAt(index, LatLng.newInstance(element.getLatitude() + latDelta, element.getLongitude() + lngDelta)); - } - }); - } - } - } - - private AdvantageLineAnimator advantageTimer = new AdvantageLineAnimator(coordinateSystem); private void showAdvantageLine(Iterable competitorsToShow, Date date) { if (map != null && lastRaceTimesInfo != null && quickRanks != null && lastCombinedWindTrackInfoDTO != null) { @@ -1349,6 +1303,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe options.setStrokeOpacity(0.5); advantageLine = Polyline.newInstance(options); + advantageTimer = new AdvantageLineAnimator(advantageLine); MVCArray pointsAsArray = MVCArray.newInstance(); pointsAsArray.insertAt(0, advantageLinePos1); pointsAsArray.insertAt(1, advantageLinePos2); From 7e26d16c270b0ae80704caa512b65f368b0682c6 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Tue, 1 Dec 2015 13:19:11 +0100 Subject: [PATCH 073/306] newSelectionModel: some general changes & Formating --- .../AbstractEventManagementPanel.java | 6 +- .../AbstractLeaderboardConfigPanel.java | 9 +- .../AbstractRaceManagementPanel.java | 5 +- .../AbstractTrackedRacesListComposite.java | 47 +-------- .../AssignRacesToMediaDialog.java | 10 +- .../DeviceConfigurationListComposite.java | 21 +--- .../DeviceConfigurationPanel.java | 7 +- .../adminconsole/LeaderboardConfigPanel.java | 11 +-- .../LeaderboardGroupConfigPanel.java | 8 +- .../adminconsole/RegattaManagementPanel.java | 1 - .../StructureImportManagementPanel.java | 2 +- .../SwissTimingEventManagementPanel.java | 2 +- .../SwissTimingReplayConnectorPanel.java | 2 +- .../TracTracEventManagementPanel.java | 2 +- .../TrackedRacesListComposite.java | 5 +- .../gwt/ui/adminconsole/WindPanel.java | 9 +- .../ui/client/SelectionChangeListener.java | 19 ---- .../gwt/ui/client/SelectionProvider.java | 27 ----- .../gwt/ui/client/SelectionProviderImpl.java | 98 ------------------- .../controls/SelectionCheckboxColumn.java | 21 +++- .../RefreshableMultiSelectionModel.java | 2 - .../RefreshableSingleSelectionModel.java | 2 - 22 files changed, 48 insertions(+), 268 deletions(-) delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionChangeListener.java delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProvider.java delete mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProviderImpl.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java index 2f350499fa8..d0d0dad3dba 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractEventManagementPanel.java @@ -29,7 +29,7 @@ public abstract class AbstractEventManagementPanel extends AbstractRegattaPanel private final ListBox availableRegattasListBox; public AbstractEventManagementPanel(SailingServiceAsync sailingService, RegattaRefresher regattaRefresher, - ErrorReporter errorReporter/*, RaceSelectionProvider raceSelectionProvider*/, boolean actionButtonsEnabled, StringMessages stringMessages) { + ErrorReporter errorReporter, boolean actionButtonsEnabled, StringMessages stringMessages) { super(sailingService, regattaRefresher, errorReporter, stringMessages); this.availableRegattas = new ArrayList(); @@ -37,8 +37,8 @@ public abstract class AbstractEventManagementPanel extends AbstractRegattaPanel this.availableRegattasListBox.ensureDebugId("AvailableRegattasListBox"); // TrackedEventsComposite should exist in every *ManagementPanel. - trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher/*, - raceSelectionProvider*/, stringMessages, /* multiselection */ true, actionButtonsEnabled); + trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher, + stringMessages, /* multiselection */ true, actionButtonsEnabled); trackedRacesListComposite.ensureDebugId("TrackedRacesListComposite"); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index 380894dffa6..b11071504b8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -56,7 +56,7 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; public abstract class AbstractLeaderboardConfigPanel extends FormPanel implements SelectedLeaderboardProvider, - RegattasDisplayer, /*RaceSelectionChangeListener, */TrackedRaceChangedListener, LeaderboardsDisplayer { + RegattasDisplayer, TrackedRaceChangedListener, LeaderboardsDisplayer { protected final VerticalPanel mainPanel; protected final TrackedRacesListComposite trackedRacesListComposite; @@ -86,7 +86,6 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement protected final RefreshableMultiSelectionModel refreshableLeaderboardSelectionModel; - //protected final RaceSelectionProvider raceSelectionProvider; protected final RefreshableSingleSelectionModel raceSingelSelectionModel; protected boolean reactOnSelectionChangeEvent; @@ -214,9 +213,8 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement trackedRacesCaptionPanel.setContentWidget(trackedRacesPanel); trackedRacesCaptionPanel.setStyleName("bold"); - //raceSelectionProvider = new RaceSelectionModel(); - trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher,/* - raceSelectionProvider, */stringMessages, /* multiselection */false, isActionButtonsEnabled()); + trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher, + stringMessages, /* multiselection */false, isActionButtonsEnabled()); trackedRacesListComposite.ensureDebugId("TrackedRacesListComposite"); trackedRacesPanel.add(trackedRacesListComposite); trackedRacesListComposite.addTrackedRaceChangeListener(this); @@ -316,7 +314,6 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement availableLeaderboardList.clear(); Util.addAll(leaderboards, availableLeaderboardList); filterLeaderboardPanel.updateAll(availableLeaderboardList); // also maintains the filtered leaderboardList - //leaderboardSelectionChanged(); refreshableLeaderboardSelectionModel.refreshSelectionModel(leaderboards); leaderboardRaceColumnSelectionChanged(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java index c240585f823..4651e402fcd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java @@ -15,7 +15,7 @@ import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.RegattaDTO; import com.sap.sse.gwt.client.ErrorReporter; -public abstract class AbstractRaceManagementPanel extends AbstractEventManagementPanel /*implements RaceSelectionChangeListener*/ { +public abstract class AbstractRaceManagementPanel extends AbstractEventManagementPanel { protected RegattaAndRaceIdentifier singleSelectedRace; protected RaceDTO selectedRaceDTO; @@ -28,7 +28,7 @@ public abstract class AbstractRaceManagementPanel extends AbstractEventManagemen public AbstractRaceManagementPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, RegattaRefresher regattaRefresher, boolean actionButtonsEnabled, StringMessages stringMessages) { - super(sailingService, regattaRefresher, errorReporter, /*new RaceSelectionModel(), */actionButtonsEnabled, stringMessages); + super(sailingService, regattaRefresher, errorReporter, actionButtonsEnabled, stringMessages); VerticalPanel mainPanel = new VerticalPanel(); this.setWidget(mainPanel); @@ -36,7 +36,6 @@ public abstract class AbstractRaceManagementPanel extends AbstractEventManagemen mainPanel.add(trackedRacesListComposite); - //trackedRacesListComposite.addRaceSelectionChangeListener(this); trackedRacesListComposite.getSelectionModel().addSelectionChangeHandler(new Handler() { @Override 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 a2b7a6b628f..b4b2e7bb611 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 @@ -50,14 +50,12 @@ import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; public abstract class AbstractTrackedRacesListComposite extends SimplePanel implements Component, - RegattasDisplayer/*, RaceSelectionChangeListener*/ { + RegattasDisplayer { protected final long DEFAULT_LIVE_DELAY_IN_MILLISECONDS = 5000; private final boolean multiSelection; - //private boolean dontFireNextSelectionChangeEvent; - protected RefreshableSelectionModel refreshableSelectionModel; private SelectionCheckboxColumn selectionCheckboxColumn; @@ -73,7 +71,6 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl protected final SailingServiceAsync sailingService; protected final ErrorReporter errorReporter; protected final RegattaRefresher regattaRefresher; - //private final RaceSelectionProvider raceSelectionProvider; protected final StringMessages stringMessages; private Button btnRefresh; @@ -91,12 +88,11 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl public AbstractTrackedRacesListComposite(final SailingServiceAsync sailingService, final ErrorReporter errorReporter, final RegattaRefresher regattaRefresher, - /*final RaceSelectionProvider raceSelectionProvider, */final StringMessages stringMessages, boolean hasMultiSelection) { + final StringMessages stringMessages, boolean hasMultiSelection) { this.sailingService = sailingService; this.errorReporter = errorReporter; this.regattaRefresher = regattaRefresher; this.multiSelection = hasMultiSelection; -// this.raceSelectionProvider = raceSelectionProvider; this.stringMessages = stringMessages; } @@ -166,18 +162,6 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl public void onSelectionChange(SelectionChangeEvent event) { Set selectedRaces = refreshableSelectionModel.getSelectedSet(); makeControlsReactToSelectionChange(selectedRaces); - /*if (dontFireNextSelectionChangeEvent) { - dontFireNextSelectionChangeEvent = false; - } else { - - List selectedRaceIdentifiers = new ArrayList(); - for (RaceDTO selectedRace : selectedRaces) { - selectedRaceIdentifiers.add(selectedRace.getRaceIdentifier()); - } - AbstractTrackedRacesListComposite.this.raceSelectionProvider.setSelection(selectedRaceIdentifiers, - AbstractTrackedRacesListComposite.this); - - }*/ } }); filterablePanelRaces = new LabeledAbstractFilterablePanel(lblFilterRaces, allRaces, raceTable, raceList) { @@ -394,14 +378,6 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl return columnSortHandler; } - /* - @Override - public void onRaceSelectionChange(List selectedRaces) { - for (RaceDTO raceFromAllRaces : raceList.getList()) { - selectionModel.setSelected(raceFromAllRaces, selectedRaces.contains(raceFromAllRaces.getRaceIdentifier())); - } - } -*/ @Override public boolean hasSettings() { return true; @@ -468,26 +444,13 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl } return result; } -/* - List getSelectedRaces() { - List result = new ArrayList(); - if (raceList != null) { - for (RaceDTO race : raceList.getList()) { - if (selectionModel.isSelected(race)) { - result.add(race); - } - } - } - 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())) { - //dontFireNextSelectionChangeEvent = true; refreshableSelectionModel.setSelected(race, true); break; } @@ -511,22 +474,18 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl } List newAllRaces = new ArrayList(); List newAllRegattas = new ArrayList(); - //List newAllRaceIdentifiers = new ArrayList(); for (RegattaDTO regatta : regattas) { newAllRegattas.add(regatta); for (RaceDTO race : regatta.races) { if (race != null) { if (raceIsToBeAddedToList(race)) { newAllRaces.add(race); - //newAllRaceIdentifiers.add(race.getRaceIdentifier()); } } } } allRaces = newAllRaces; filterablePanelRaces.updateAll(allRaces); - //raceSelectionProvider.setAllRaces(newAllRaceIdentifiers); // have this object be notified; triggers - // onRaceSelectionChange refreshableSelectionModel.refreshSelectionModel(allRaces); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AssignRacesToMediaDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AssignRacesToMediaDialog.java index 183b6b7f50d..d4d4d033a97 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AssignRacesToMediaDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AssignRacesToMediaDialog.java @@ -37,8 +37,8 @@ public class AssignRacesToMediaDialog extends DataEntryDialog selectedRaces) { - } -*/ + @Override protected void makeControlsReactToFillRegattas(Iterable regattas) { } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java index f1c10ff2ef0..b40f5259bf2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java @@ -37,8 +37,7 @@ public class DeviceConfigurationListComposite extends Composite { private final ErrorReporter errorReporter; protected final StringMessages stringMessages; - public DeviceConfigurationListComposite(final SailingServiceAsync sailingService, - /*final SelectionProvider selectionProvider,*/ + public DeviceConfigurationListComposite(final SailingServiceAsync sailingService, final RefreshableSelectionModel refreshableSelectionModel, final ErrorReporter errorReporter, final StringMessages stringMessages) { this.sailingService = sailingService; @@ -60,12 +59,6 @@ public class DeviceConfigurationListComposite extends Composite { configurationTable.setVisible(true); refreshableConfigurationSelectionModel = refreshableSelectionModel; - /*refreshableConfigurationSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { - @Override - public void onSelectionChange(SelectionChangeEvent event) { - selectionProvider.setSelection(getSelectedConfigurations()); - } - });*/ configurationTable.setSelectionModel(refreshableConfigurationSelectionModel); panel.add(configurationTable); @@ -144,16 +137,4 @@ public class DeviceConfigurationListComposite extends Composite { table.addColumn(identifierNameColumn, stringMessages.device()); return table; } -/* - private List getSelectedConfigurations() { - List result = new ArrayList(); - if (configurationsDataProvider != null) { - for (DeviceConfigurationMatcherDTO regatta : configurationsDataProvider.getList()) { - if (refreshableConfigurationSelectionModel.isSelected(regatta)) { - result.add(regatta); - } - } - } - return result; - }*/ } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java index 8298636a9ec..92a709c8dd4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java @@ -27,7 +27,7 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; -public class DeviceConfigurationPanel extends SimplePanel implements /*SelectionChangeListener,*/ DeviceConfigurationCloneListener { +public class DeviceConfigurationPanel extends SimplePanel implements DeviceConfigurationCloneListener { public static String renderIdentifiers(List clientIdentifiers, StringMessages stringMessages) { if (clientIdentifiers.size() == 1) { @@ -49,7 +49,6 @@ public class DeviceConfigurationPanel extends SimplePanel implements /*Selection private DeviceConfigurationListComposite listComposite; private DeviceConfigurationDetailComposite detailComposite; - //private final SelectionProvider selectionProvider; private final RefreshableMultiSelectionModel refreshableMultiSelectionModel; public DeviceConfigurationPanel(SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter reporter) { @@ -73,12 +72,8 @@ public class DeviceConfigurationPanel extends SimplePanel implements /*Selection detailComposite.setVisible(false); } removeConfigurationButton.setEnabled(!selectedConfigurations.isEmpty()); - } }); - //this.selectionProvider = new SelectionProviderImpl(true); - //this.selectionProvider.addSelectionChangeListener(this); - setupUi(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java index 02f30f97ae6..e102d81bb44 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java @@ -55,7 +55,7 @@ import com.sap.sse.gwt.client.URLEncoder; import com.sap.sse.gwt.client.async.MarkedAsyncCallback; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; -public class LeaderboardConfigPanel extends AbstractLeaderboardConfigPanel implements SelectedLeaderboardProvider, RegattasDisplayer, /*RaceSelectionChangeListener,*/ +public class LeaderboardConfigPanel extends AbstractLeaderboardConfigPanel implements SelectedLeaderboardProvider, RegattasDisplayer, TrackedRaceChangedListener, LeaderboardsDisplayer { private final AnchorTemplates ANCHORTEMPLATE = GWT.create(AnchorTemplates.class); @@ -671,18 +671,9 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { protected void leaderboardSelectionChanged() { // make sure that clearing the selection doesn't cause an unlinking of the selected tracked race reactOnSelectionChangeEvent = false; - //raceSelectionProvider.removeRaceSelectionChangeListener(this); trackedRacesListComposite.clearSelection(); // add listener again using a scheduled command which is executed when the browser's event loop re-gains // control; we assume that at that point in time the selection updates have already been performed - /* - Scheduler.get().scheduleFinally(new ScheduledCommand() { - @Override - public void execute() { - raceSelectionProvider.addRaceSelectionChangeListener(LeaderboardConfigPanel.this); - } - }); - */ leaderboardRemoveButton.setEnabled(!refreshableLeaderboardSelectionModel.getSelectedSet().isEmpty()); StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard(); if (refreshableLeaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java index 13b99345245..b1b35a53d9c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java @@ -7,6 +7,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; + import com.google.gwt.cell.client.FieldUpdater; import com.google.gwt.core.client.GWT; import com.google.gwt.event.dom.client.ClickEvent; @@ -191,8 +192,11 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements leaderboardsTable.ensureDebugId("LeaderboardsCellTable"); SelectionCheckboxColumn leaderboardTableSelectionColumn = new SelectionCheckboxColumn( - tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { - @Override + tableResources.cellTableStyle().cellTableCheckboxSelected(), + tableResources.cellTableStyle().cellTableCheckboxDeselected(), + tableResources.cellTableStyle().cellTableCheckboxColumnCell(), + null /* entityIdentityComparator to create a RefreshableSelectionModel */) { + @Override protected ListDataProvider getListDataProvider() { return leaderboardsProvider; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java index 4c83d4e185d..a195b3f00fd 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java @@ -134,7 +134,6 @@ public class RegattaManagementPanel extends SimplePanel implements RegattasDispl removeRegattaButton.setEnabled(!selectedRegattas.isEmpty()); } }); - //refreshableRegattaMultiSelectionModel.addRegattaSelectionChangeListener(this); regattaListComposite = new RegattaListComposite(sailingService, refreshableRegattaMultiSelectionModel, regattaRefresher, errorReporter, stringMessages); regattaListComposite.ensureDebugId("RegattaListComposite"); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java index dbeb7ed0350..b005ad38433 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java @@ -117,8 +117,8 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat VerticalPanel mainPanel = new VerticalPanel(); setWidget(mainPanel); mainPanel.setWidth("100%"); - createUI(mainPanel); + createUI(mainPanel); regattaListComposite.addSelectionChangeHandler(new Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java index 1fa1c0b58bc..3ea91a2d459 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java @@ -66,7 +66,7 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane public SwissTimingEventManagementPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, RegattaRefresher regattaRefresher, StringMessages stringConstants) { - super(sailingService, regattaRefresher, errorReporter, /*new RaceSelectionModel(),*/ true, stringConstants); + super(sailingService, regattaRefresher, errorReporter, true, stringConstants); this.errorReporter = errorReporter; VerticalPanel mainPanel = new VerticalPanel(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java index 4e072330130..53e64474acd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java @@ -64,7 +64,7 @@ public class SwissTimingReplayConnectorPanel extends AbstractEventManagementPane public SwissTimingReplayConnectorPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, RegattaRefresher regattaRefresher, StringMessages stringMessages) { - super(sailingService, regattaRefresher, errorReporter, /*new RaceSelectionModel(),*/ true, stringMessages); + super(sailingService, regattaRefresher, errorReporter, true, stringMessages); this.errorReporter = errorReporter; availableSwissTimingRaces = new ArrayList(); previousConfigurationsComboBox = new ListBox(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java index e132e3f64a9..c47f3ef5831 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java @@ -86,7 +86,7 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { public TracTracEventManagementPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, RegattaRefresher regattaRefresher, StringMessages stringMessages) { - super(sailingService, regattaRefresher, errorReporter, /*new RaceSelectionModel(),*/ true, stringMessages); + super(sailingService, regattaRefresher, errorReporter, true, stringMessages); this.errorReporter = errorReporter; this.previousConfigurations = new HashMap(); this.availableTracTracRaces = new ArrayList(); 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 005d6ecb3b3..ea5a026932e 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 @@ -37,9 +37,8 @@ public class TrackedRacesListComposite extends AbstractTrackedRacesListComposite private boolean actionButtonsEnabled; public TrackedRacesListComposite(final SailingServiceAsync sailingService, final ErrorReporter errorReporter, - final RegattaRefresher regattaRefresher, /*RaceSelectionProvider raceSelectionProvider,*/ - final StringMessages stringMessages, boolean hasMultiSelection, boolean actionButtonsEnabled) { - super(sailingService, errorReporter, regattaRefresher/*, raceSelectionProvider*/, stringMessages, hasMultiSelection); + final RegattaRefresher regattaRefresher, final StringMessages stringMessages, boolean hasMultiSelection, boolean actionButtonsEnabled) { + super(sailingService, errorReporter, regattaRefresher, stringMessages, hasMultiSelection); this.raceIsTrackedRaceChangeListener = new HashSet(); this.actionButtonsEnabled = actionButtonsEnabled; createUI(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java index 6d9872bda52..f782bb0885b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java @@ -75,7 +75,7 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; * @author Axel Uhl (d043530) * */ -public class WindPanel extends FormPanel implements RegattasDisplayer, WindShower/*, RaceSelectionChangeListener*/ { +public class WindPanel extends FormPanel implements RegattasDisplayer, WindShower { private static final String EXPEDITON_IMPORT_PARAMETER_RACES = "races"; @@ -92,7 +92,6 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe private final TextColumn windDirectionInDegColumn; private final TextColumn positionColumn; private final TrackedRacesListComposite trackedRacesListComposite; -// private final RaceSelectionProvider raceSelectionProvider; private final RefreshableMultiSelectionModel refreshableRaceSelectionModel; private final WindSourcesToExcludeSelectorPanel windSourcesToExcludeSelectorPanel; private final CheckBox raceIsKnownToStartUpwindBox; @@ -108,17 +107,15 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe this.sailingService = sailingService; this.errorReporter = errorReporter; this.stringMessages = stringMessages; -// this.raceSelectionProvider = new RaceSelectionModel(); windSourcesToExcludeSelectorPanel = new WindSourcesToExcludeSelectorPanel(sailingService, stringMessages, errorReporter); VerticalPanel mainPanel = new VerticalPanel(); mainPanel.setSize("100%", "100%"); this.setWidget(mainPanel); - trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher/*, - raceSelectionProvider*/, stringMessages, /*multiselection*/true, /* actionButtonsEnabled */ false); + trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher, + stringMessages, /*multiselection*/true, /* actionButtonsEnabled */ false); mainPanel.add(trackedRacesListComposite); - //raceSelectionProvider.addRaceSelectionChangeListener(this); refreshableRaceSelectionModel = (RefreshableMultiSelectionModel) trackedRacesListComposite.getSelectionModel(); refreshableRaceSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionChangeListener.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionChangeListener.java deleted file mode 100644 index 4ad12c87def..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionChangeListener.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.List; - -/** - * Allows UI components to observe a selector, such as a drop-down box showing a list of items. - * - * - */ -public interface SelectionChangeListener { - - /** - * The first element is the first one selected - * - * @param selectedItems a non-null list which is empty if nothing is selected - */ - void onSelectionChange(List selectedItems); - -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProvider.java deleted file mode 100644 index d3fc2ff1af5..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProvider.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.List; - -public interface SelectionProvider { - /** - * @return a non-null list which may be empty - */ - List getSelectedItems(); - - void addSelectionChangeListener(SelectionChangeListener listener); - - void removeSelectionChangeListener(SelectionChangeListener listener); - - void setSelection(List newSelection); - - void setSelection(List newSelection, SelectionChangeListener[] listenerNotToNotify); - - boolean hasMultiSelection(); - - /** - * @return a non-null list which may be empty - */ - List getAllItems(); - - void setAllItems(List newAllItems); -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProviderImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProviderImpl.java deleted file mode 100644 index ff60a1b9821..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SelectionProviderImpl.java +++ /dev/null @@ -1,98 +0,0 @@ -/*package com.sap.sailing.gwt.ui.client; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -public class SelectionProviderImpl implements SelectionProvider { - private final List selection; - - private final List allItems; - - private final Set> listeners; - - private final boolean hasMultiSelection; - - public SelectionProviderImpl(boolean hasMultiSelection) { - this.hasMultiSelection = hasMultiSelection; - this.selection = new ArrayList(); - this.allItems = new ArrayList(); - listeners = new HashSet>(); - } - - @Override - public List getSelectedItems() { - return Collections.unmodifiableList(selection); - } - - @Override - public void addSelectionChangeListener(SelectionChangeListener listener) { - listeners.add(listener); - } - - @Override - public void removeSelectionChangeListener(SelectionChangeListener listener) { - listeners.remove(listener); - } - - @Override - public void setSelection(List newSelection) { - setSelection(newSelection, null); - } - - @Override - public void setSelection(List newSelection, SelectionChangeListener[] listenersNotToNotify) { - boolean notify = !selection.equals(newSelection); - selection.clear(); - selection.addAll(newSelection); - if (notify) { - notifyListeners(listenersNotToNotify); - } - } - - private void notifyListeners(SelectionChangeListener[] listenersNotToNotify) { - List selectedItems = getSelectedItems(); - for (SelectionChangeListener listener : listeners) { - if (listenersNotToNotify == null || !Arrays.asList(listenersNotToNotify).contains(listener)) { - listener.onSelectionChange(selectedItems); - } - } - } - - *//** - * Sets the universe of all items from which this selection model may select. An equal list will subsequently be - * returned by {@link #getAllItems()}. Items from the {@link #getSelectedItems() selection} not in - * newAllItems are removed from the selection. If this happens, the selection listeners are notified. - *//* - @Override - public void setAllItems(List newAllItems) { - allItems.clear(); - for (T r : newAllItems) { - allItems.add(r); - } - for (Iterator i = selection.iterator(); i.hasNext(); ) { - T selectedItem = i.next(); - if (!allItems.contains(selectedItem)) { - i.remove(); - } - } - // when setting all items, the underlying items will usually have changed their identity and maybe also their state; - // so notifying the selection listeners is necessary anyhow - notifyListeners(null); - } - - @Override - public List getAllItems() { - return Collections.unmodifiableList(allItems); - } - - @Override - public boolean hasMultiSelection() { - return hasMultiSelection; - } -} -*/ \ 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/controls/SelectionCheckboxColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java index 6d1e80a9ce1..fa6475f0dad 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java @@ -21,6 +21,7 @@ import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; import com.sap.sailing.domain.common.impl.InvertibleComparatorAdapter; import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; /** * A column to be used in a {@link CellTable} that controls and reflects a table's selection model using stylable @@ -50,6 +51,21 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW private final EventTranslator selectionEventTranslator; private final RefreshableMultiSelectionModel selectionModel; + /** + * @param selectionmodel + * {@link RefreshableSelectionModel} to set an already existing selectionmodel for the + * {@link SelectionCheckboxColumn} + * @param selectedCheckboxCSSClass + * CSS class for the <div> element representing a selected element + * @param deselectedCheckboxCSSClass + * CSS class for the <div> element representing a deselected element + * @param checkboxColumnCellCSSClass + * CSS class for the <td> element rendering the cell + */ + protected SelectionCheckboxColumn(RefreshableMultiSelectionModel selectionmodel, String selectedCheckboxCSSClass, + String deselectedCheckboxCSSClass, String checkboxColumnCellCSSClass) { + this(selectionmodel, new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass); + } /** * @param selectedCheckboxCSSClass * CSS class for the <div> element representing a selected element @@ -60,11 +76,6 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW * @param entityIdentityComparator * {@link EntityIdentityComparator} to create a {@link RefreshableMultiSelectionModel}. */ - protected SelectionCheckboxColumn(RefreshableMultiSelectionModel selectionmodel, String selectedCheckboxCSSClass, - String deselectedCheckboxCSSClass, String checkboxColumnCellCSSClass) { - this(selectionmodel, new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass); - } - protected SelectionCheckboxColumn(String selectedCheckboxCSSClass, String checkboxColumnCellCSSClass, String deselectedCheckboxCSSClass, EntityIdentityComparator entityIdentityComparator) { this(new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass, diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 4e544926fd0..381ac93980c 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -45,8 +45,6 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im } setSelected(it, isSelected); } - //setEventScheduled(true); - //fireSelectionChangeEvent(); SelectionChangeEvent.fire(this); } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index 268d9e78e6f..bf198378979 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -39,8 +39,6 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel } } } - //setEventScheduled(true); - //fireSelectionChangeEvent(); SelectionChangeEvent.fire(this); } } \ No newline at end of file From d0c1791c49bead81c5f794a5133db717b7bdbdf1 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 1 Dec 2015 13:23:24 +0100 Subject: [PATCH 074/306] Adjusted alignment of busy indicator in fullscreen leaderboard --- .../shared/partials/fullscreen/FullscreenContainer.ui.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml index daf2634a4a2..2fcfd38df77 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/partials/fullscreen/FullscreenContainer.ui.xml @@ -52,6 +52,10 @@ } .toolbarBusyIndicator { display: inline-block; + height: 50px; + width: auto; + padding: 10px; + vertical-align: top; } .toolbarAction:hover { background-color: #d79900; From dddbb1663ca37f8f7248d6e1a818a62282af1321 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 1 Dec 2015 14:05:41 +0100 Subject: [PATCH 075/306] Adjusted positioning of busy indicator in leaderboard tabs --- .../desktop/partials/old/EventRegattaLeaderboard.gss | 2 ++ .../desktop/partials/old/leaderboard/OldLeaderboard.java | 5 +---- .../partials/old/leaderboard/OldLeaderboard.ui.xml | 9 ++++----- .../old/multileaderboard/OldMultiLeaderboard.java | 5 +---- .../old/multileaderboard/OldMultiLeaderboard.ui.xml | 9 ++++----- 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss index 32ed3aa2a5d..0eaa3b013d7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/EventRegattaLeaderboard.gss @@ -107,6 +107,8 @@ } .regattaleaderboard_meta_busyindicator { + display: inline-block; + margin: 0 0.666666666666667em; } .regattaleaderboard_meta_liverace { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java index 3f684efce7e..2fb55ac6429 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.java @@ -33,7 +33,6 @@ import com.sap.sailing.gwt.ui.leaderboard.LeaderboardSettings; import com.sap.sailing.gwt.ui.leaderboard.ScoringSchemeTypeFormatter; 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.SimpleBusyIndicator; 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; @@ -55,13 +54,12 @@ public class OldLeaderboard extends Composite implements BusyStateChangeListener @UiField DivElement lastScoringCommentDiv; @UiField DivElement scoringSchemeDiv; @UiField DivElement hasLiveRaceDiv; - @UiField DivElement busyIndicatorDiv; + @UiField BusyIndicator busyIndicator; @UiField EventRegattaLeaderboardResources local_res; private LeaderboardPanel leaderboardPanel; private Timer autoRefreshTimer; private final OldLeaderboardDelegate delegate; - private final BusyIndicator busyIndicator = new SimpleBusyIndicator(); public OldLeaderboard() { this(null); @@ -74,7 +72,6 @@ public class OldLeaderboard extends Composite implements BusyStateChangeListener settingsAnchor.setTitle(StringMessages.INSTANCE.settings()); autoRefreshAnchor.setTitle(StringMessages.INSTANCE.refresh()); fullscreenAnchor.setTitle(StringMessages.INSTANCE.openFullscreenView()); - busyIndicatorDiv.appendChild(busyIndicator.getElement()); this.delegate = delegate; this.setupFullscreenDelegate(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml index e6bfdd6861f..a65cf15c133 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/leaderboard/OldLeaderboard.ui.xml @@ -1,5 +1,6 @@ - + @@ -10,10 +11,8 @@

    -
    -
    -
    -
    +
    +
    diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java index c07abafffc3..0cb578288a0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.java @@ -34,7 +34,6 @@ import com.sap.sailing.gwt.ui.leaderboard.ScoringSchemeTypeFormatter; import com.sap.sailing.gwt.ui.leaderboard.SelectedLeaderboardChangeListener; 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.SimpleBusyIndicator; 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; @@ -55,13 +54,12 @@ public class OldMultiLeaderboard extends Composite implements SelectedLeaderboar @UiField DivElement lastScoringUpdateTextDiv; @UiField DivElement lastScoringCommentDiv; @UiField DivElement scoringSchemeDiv; - @UiField DivElement busyIndicatorDiv; + @UiField BusyIndicator busyIndicator; @UiField EventRegattaLeaderboardResources local_res; private MultiLeaderboardPanel multiLeaderboardPanel; private Timer autoRefreshTimer; private final OldMultiLeaderboardDelegate delegate; - private final BusyIndicator busyIndicator = new SimpleBusyIndicator(); private LeaderboardPanel lastSelectedLeaderboardPanel; public OldMultiLeaderboard() { @@ -77,7 +75,6 @@ public class OldMultiLeaderboard extends Composite implements SelectedLeaderboar fullscreenAnchor.setTitle(StringMessages.INSTANCE.openFullscreenView()); this.delegate = delegate; this.setupFullscreenDelegate(); - busyIndicatorDiv.appendChild(busyIndicator.getElement()); lastSelectedLeaderboardPanel = null; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml index 1fa03514f31..55a4cf060f9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/old/multileaderboard/OldMultiLeaderboard.ui.xml @@ -1,5 +1,6 @@ - + @@ -9,10 +10,8 @@
    -
    -
    -
    -
    +
    +
    From 4a2706053711e03824bf283a35fdb40f7598ec2a Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Tue, 1 Dec 2015 15:01:16 +0100 Subject: [PATCH 076/306] newSelectionModel: JavaDoc & revised .refreshSelectionModel(newObjects) --- .../RefreshableMultiSelectionModel.java | 16 ++++++++++------ .../celltable/RefreshableSelectionModel.java | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 381ac93980c..1d88541c66e 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -36,14 +36,18 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im clear(); if (isNotEmpty) { for (T it : newObjects) { - boolean isSelected = false; - for (T selected : selectedSet) { - isSelected = (comp == null ? selected.equals(it) : comp.representSameEntity(selected, it)); - if (isSelected) { - break; + if (comp == null) { + setSelected(it, selectedSet.contains(it)); + } else { + boolean isSelected = false; + for (T selected : selectedSet) { + isSelected = comp.representSameEntity(selected, it); + if (isSelected) { + break; + } } + setSelected(it, isSelected); } - setSelected(it, isSelected); } SelectionChangeEvent.fire(this); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java index 888a7144da5..a38dfd576c5 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java @@ -1,5 +1,6 @@ package com.sap.sse.gwt.client.celltable; +import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SetSelectionModel; import com.google.gwt.view.client.SingleSelectionModel; @@ -8,9 +9,11 @@ import com.google.gwt.view.client.SingleSelectionModel; * The interface {@link RefreshableSelectionModel} unifies the refresh behavior of SelectionModels e.g. of a * {@link SingleSelectionModel}. To refresh this SelectionModel you can call the * {@link RefreshableSelectionModel#refreshSelectionModel(Iterable)} method and this method will refresh the - * SelectionModel. + * SelectionModel. The refresh should be triggered after the {@link ListDataProvider} was updated. + *

    * - * TODO Lukas, please enhance this comment to explain how this re-establishes the invariant of same elements in SetSelectionModel.getSelectedElements and DataProvider + * TODO Lukas, please enhance this comment to explain how this re-establishes the invariant of same elements in + * SetSelectionModel.getSelectedElements and DataProvider * * @author D064976 */ @@ -25,16 +28,15 @@ public interface RefreshableSelectionModel extends SetSelectionModel { /** * Refreshes the {@link RefreshableSelectionModel} with the newObjects. All objects from the current * selection that {@link EntityIdentityComparator#representSameEntity(Object, Object) represent the same entity} as - * an object from newObjects will be reselected. All others are de-selected. If this selection model - * has no {@link EntityIdentityComparator} set, this method will use the {@link #equals(Object)} method to compare. - *

    - * - * If a selected object is not contained in newObjects the object wouldn't be selected anymore. + * an object from newObjects will be reselected. All others are de-selected. That means a selected + * object is not contained in newObjects the object wouldn't be selected anymore. If this selection + * model has no {@link EntityIdentityComparator} set, this method will use the {@link #equals(Object)} method to + * compare. *

    * * When the selection is refreshed this method triggers a * {@link SelectionChangeEvent.Handler#onSelectionChange(SelectionChangeEvent) onSelectionChangedEvent} using - * {@link AbstractSelectionModel#scheduleSelectionChangeEvent}. + * {@link AbstractSelectionModel#fireEvent(com.google.gwt.event.shared.GwtEvent)}. */ public void refreshSelectionModel(Iterable newObjects); } From f62c09f821df41dc1559ab1718bb6755963d4e8a Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 1 Dec 2015 21:39:21 +0100 Subject: [PATCH 077/306] working towards reducing the number of GetRaceMapData calls; however, this version is INCORRECT; see TODO comments --- .../gwt/ui/actions/GetRaceMapDataAction.java | 2 - .../client/shared/racemap/FixesAndTails.java | 13 +++-- .../gwt/ui/client/shared/racemap/RaceMap.java | 47 +++++++++++++------ 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceMapDataAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceMapDataAction.java index ac35428be2a..fd6895658cf 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceMapDataAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/actions/GetRaceMapDataAction.java @@ -42,12 +42,10 @@ public class GetRaceMapDataAction implements AsyncAction { for (Map.Entry fromEntry : from.entrySet()) { fromByCompetitorIdAsString.put(fromEntry.getKey().getIdAsString(), fromEntry.getValue()); } - Map toByCompetitorIdAsString = new HashMap(); for (Map.Entry toEntry : to.entrySet()) { toByCompetitorIdAsString.put(toEntry.getKey().getIdAsString(), toEntry.getValue()); } - sailingService.getRaceMapData(raceIdentifier, date, fromByCompetitorIdAsString, toByCompetitorIdAsString, extrapolate, simulationLegIdentifier, new AsyncCallback() { @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/FixesAndTails.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/FixesAndTails.java index 492a17635cb..626f0d60702 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/FixesAndTails.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/FixesAndTails.java @@ -5,6 +5,7 @@ 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.List; import java.util.ListIterator; @@ -178,18 +179,23 @@ public class FixesAndTails { * overlap with those already known, the fixes are merged into the list of already known fixes for the competitor. * Otherwise, the fixes received in result replace those known so far for the respective competitor. * The {@link #tails} affected by these fixes are updated accordingly when modifications fall inside the interval - * shown by the tail, as defined by {@link #firstShownFix} and {@link #lastShownFix}. The tails are, however, - * not trimmed according to the specification for the tail length. This has to happen elsewhere (see also + * shown by the tail, as defined by {@link #firstShownFix} and {@link #lastShownFix}. The tails are, however, not + * trimmed according to the specification for the tail length. This has to happen elsewhere (see also * {@link #updateTail}). * * @param fixesForCompetitors * For each list the invariant must hold that an {@link GPSFixDTO#extrapolated extrapolated} fix must be * the last one in the list + * + * @return the competitors for which new fixes have been added; this will not include competitors appearing as keys + * in foxesForCompetitor but with a null or empty fixes list */ - protected void updateFixes(Map> fixesForCompetitors, + protected Iterable updateFixes(Map> fixesForCompetitors, Map overlapsWithKnownFixes, TailFactory tailFactory, long timeForPositionTransitionMillis) { + final Set competitorsWithNewFixes = new HashSet<>(); for (Map.Entry> e : fixesForCompetitors.entrySet()) { if (e.getValue() != null && !e.getValue().isEmpty()) { + competitorsWithNewFixes.add(e.getKey()); List fixesForCompetitor = fixes.get(e.getKey()); if (fixesForCompetitor == null) { fixesForCompetitor = new ArrayList(); @@ -212,6 +218,7 @@ public class FixesAndTails { } } } + return competitorsWithNewFixes; } /** 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 b4657f0d566..ae3900b60d1 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 @@ -772,18 +772,25 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe // For those competitors for which the tails don't overlap (and therefore will be replaced by the new tail coming from the server) // we expect some potential delay in computing the full tail. Therefore, in those cases we fire two requests: one fetching only the // boat positions at newTime with zero tail length; and another one fetching everything else. + // TODO bug3378: two calls that both request simulation and mark data wastes bandwidth and CPU; one of them should only use getBoatPositions to request missing pieces of tails; showBoatsOnMap must not remove canvases / tails if one of the two calls delivers only partial results GetRaceMapDataAction getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping = getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping(fromAndToAndOverlap, race, newTime); + final boolean updateMarksAndLinesExceptAdvantageLineInCallback; if (getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping != null) { asyncActionsExecutor.execute(getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping, GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); + getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID, + /* updateMarksAndLinesExceptAdvantageLineInCallback */ true)); + updateMarksAndLinesExceptAdvantageLineInCallback = false; // don't do this again in the second call that mainly fetches requestID = ++boatPositionRequestIDCounter; + } else { + updateMarksAndLinesExceptAdvantageLineInCallback = true; } // next, do the full thing; being the later call, if request throttling kicks in, the later call // supersedes the earlier call which may get dropped then GetRaceMapDataAction getRaceMapDataAction = new GetRaceMapDataAction(sailingService, competitorSelection.getAllCompetitors(), race, useNullAsTimePoint() ? null : newTime, fromAndToAndOverlap.getA(), fromAndToAndOverlap.getB(), /* extrapolate */ true, (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null)); asyncActionsExecutor.execute(getRaceMapDataAction, GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); + getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID, + updateMarksAndLinesExceptAdvantageLineInCallback)); // draw the wind into the map, get the combined wind List windSourceTypeNames = new ArrayList(); @@ -868,11 +875,18 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe return result; } + /** + * @param updateMarksAndLinesExceptAdvantageLineInCallback + * when true, mark positions, mark-related lines such as start line and course middle lines, + * three hull length circles and side lines are drawn; otherwise, only the boats, their info flags and + * the advantage line will be updated. + */ private AsyncCallback getRaceMapDataCallback( final Date oldTime, final Date newTime, final Map hasTailOverlapForCompetitor, - final Iterable competitorsToShow, final int requestID) { + final Iterable competitorsToShow, final int requestID, + final boolean updateMarksAndLinesExceptAdvantageLineInCallback) { return new AsyncCallback() { @Override public void onFailure(Throwable caught) { @@ -895,9 +909,15 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe // Do boat specific actions Map> boatData = raceMapDataDTO.boatPositions; long timeForPositionTransitionMillis = calculateTimeForPositionTransition(newTime, oldTime); - fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, timeForPositionTransitionMillis); - showBoatsOnMap(newTime, timeForPositionTransitionMillis, getCompetitorsToShow()); - showCompetitorInfoOnMap(newTime, timeForPositionTransitionMillis, competitorSelection.getSelectedFilteredCompetitors()); + Iterable competitorsForWhichNewFixesWereReveiced = + fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, timeForPositionTransitionMillis); + if (!Util.isEmpty(competitorsForWhichNewFixesWereReveiced)) { + showBoatsOnMap(newTime, timeForPositionTransitionMillis, competitorsForWhichNewFixesWereReveiced); + showCompetitorInfoOnMap(newTime, timeForPositionTransitionMillis, 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); + } if (douglasMarkers != null) { removeAllMarkDouglasPeuckerpoints(); } @@ -905,17 +925,16 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe removeAllManeuverMarkers(); } - // Do mark specific actions - showCourseMarksOnMap(raceMapDataDTO.coursePositions); if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { updateCoordinateSystemFromSettings(); } - showCourseSidelinesOnMap(raceMapDataDTO.courseSidelines); - showStartAndFinishAndCourseMiddleLines(raceMapDataDTO.coursePositions); - showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); - // 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); + if (updateMarksAndLinesExceptAdvantageLineInCallback) { + // Do mark specific actions + showCourseMarksOnMap(raceMapDataDTO.coursePositions); + showCourseSidelinesOnMap(raceMapDataDTO.courseSidelines); + showStartAndFinishAndCourseMiddleLines(raceMapDataDTO.coursePositions); + showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); + } // Rezoom the map LatLngBounds zoomToBounds = null; From 92cbc2475aa87a0ff81dc9e4c05714592fa815f6 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 2 Dec 2015 10:25:01 +0100 Subject: [PATCH 078/306] removed redundant getRefreshableSelectionModel method and solved by adjusting type parameters to Refreshable*SelectionModel instead --- .../AbstractCompetitorRegistrationsDialog.java | 10 +++++----- .../AbstractLeaderboardConfigPanel.java | 18 ++++++++---------- ...ractLogImportFixesAndAddMappingsDialog.java | 7 ++++--- .../gwt/ui/adminconsole/CompetitorPanel.java | 3 +-- .../adminconsole/CompetitorTableWrapper.java | 3 +-- .../adminconsole/ControlPointTableWrapper.java | 3 +-- .../CopyCourseAndCompetitorsDialog.java | 6 +++--- .../adminconsole/CourseManagementWidget.java | 16 ++++++++-------- .../DeviceIdentifierTableWrapper.java | 4 ++-- .../DeviceMappingTableWrapper.java | 4 ++-- .../ui/adminconsole/GateCreationDialog.java | 6 +++--- .../ItemToMapToDeviceSelectionPanel.java | 7 ++++--- .../gwt/ui/adminconsole/MarkTableWrapper.java | 3 +-- .../gwt/ui/adminconsole/RaceTableWrapper.java | 7 +------ ...RegattaLogTrackingDeviceMappingsDialog.java | 1 - .../gwt/ui/adminconsole/TableWrapper.java | 5 ++--- .../adminconsole/WaypointCreationDialog.java | 6 +++--- .../ui/adminconsole/WaypointTableWrapper.java | 3 +-- .../controls/SelectionCheckboxColumn.java | 9 +++++---- .../RefreshableMultiSelectionModel.java | 11 +++++++++++ 20 files changed, 66 insertions(+), 66 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorRegistrationsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorRegistrationsDialog.java index 406415114dc..dc97fa861dc 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorRegistrationsDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorRegistrationsDialog.java @@ -17,18 +17,18 @@ import com.google.gwt.user.client.ui.HasVerticalAlignment; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.view.client.MultiSelectionModel; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.CompetitorDTOImpl; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.common.Util; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog; public abstract class AbstractCompetitorRegistrationsDialog extends DataEntryDialog> { - protected CompetitorTableWrapper> allCompetitorsTable; - protected CompetitorTableWrapper> registeredCompetitorsTable; + protected CompetitorTableWrapper> allCompetitorsTable; + protected CompetitorTableWrapper> registeredCompetitorsTable; protected final boolean filterByLeaderBoardInitially = false; private final StringMessages stringMessages; protected final SailingServiceAsync sailingService; @@ -145,8 +145,8 @@ public abstract class AbstractCompetitorRegistrationsDialog extends DataEntryDia } } - private void moveSelected(CompetitorTableWrapper> from, - CompetitorTableWrapper> to) { + private void moveSelected(CompetitorTableWrapper> from, + CompetitorTableWrapper> to) { move(from, to, from.getSelectionModel().getSelectedSet()); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index b11071504b8..d5207dd8ae9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -25,7 +25,6 @@ import com.google.gwt.user.client.ui.VerticalPanel; 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.SetSelectionModel; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaNameAndRaceName; import com.sap.sailing.domain.common.dto.FleetDTO; @@ -71,7 +70,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement protected final CellTable leaderboardTable; - protected final RaceTableWrapper> raceColumnTable; + protected final RaceTableWrapper> raceColumnTable; protected final RefreshableSelectionModel raceColumnTableSelectionModel; protected RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceInLeaderboard; @@ -86,7 +85,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement protected final RefreshableMultiSelectionModel refreshableLeaderboardSelectionModel; - protected final RefreshableSingleSelectionModel raceSingelSelectionModel; + protected final RefreshableSingleSelectionModel raceSingleSelectionModel; protected boolean reactOnSelectionChangeEvent; private final LeaderboardsRefresher leaderboardsRefresher; @@ -218,16 +217,15 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement trackedRacesListComposite.ensureDebugId("TrackedRacesListComposite"); trackedRacesPanel.add(trackedRacesListComposite); trackedRacesListComposite.addTrackedRaceChangeListener(this); - this.raceSingelSelectionModel = (RefreshableSingleSelectionModel) trackedRacesListComposite.getSelectionModel(); - this.raceSingelSelectionModel.addSelectionChangeHandler(new Handler() { - + this.raceSingleSelectionModel = (RefreshableSingleSelectionModel) trackedRacesListComposite.getSelectionModel(); + this.raceSingleSelectionModel.addSelectionChangeHandler(new Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { if (reactOnSelectionChangeEvent) { // if no leaderboard column is selected, ignore the race selection change RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceColumnAndFleetName = getSelectedRaceColumnWithFleet(); if (selectedRaceColumnAndFleetName != null) { - if (raceSingelSelectionModel.getSelectedSet().isEmpty()) { + if (raceSingleSelectionModel.getSelectedSet().isEmpty()) { if (selectedRaceColumnAndFleetName.getA() .getRaceIdentifier(selectedRaceColumnAndFleetName.getB()) != null) { unlinkRaceColumnFromTrackedRace( @@ -237,7 +235,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement } else { linkTrackedRaceToSelectedRaceColumn(selectedRaceColumnAndFleetName.getA(), selectedRaceColumnAndFleetName.getB(), - raceSingelSelectionModel.getSelectedObject().getRaceIdentifier()); + raceSingleSelectionModel.getSelectedObject().getRaceIdentifier()); } } } @@ -261,11 +259,11 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement vPanel.add(reloadAllRaceLogs); Label lblRaceNamesIn = new Label(stringMessages.races()); vPanel.add(lblRaceNamesIn); - raceColumnTable = new RaceTableWrapper>( + raceColumnTable = new RaceTableWrapper>( sailingService, stringMessages, errorReporter, multiSelection); raceColumnTable.getTable().setWidth("100%"); addColumnsToRacesTable(raceColumnTable.getTable()); - this.raceColumnTableSelectionModel = raceColumnTable.getRefreshableSelectionModel(); + this.raceColumnTableSelectionModel = raceColumnTable.getSelectionModel(); raceColumnTableSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { public void onSelectionChange(SelectionChangeEvent event) { leaderboardRaceColumnSelectionChanged(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLogImportFixesAndAddMappingsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLogImportFixesAndAddMappingsDialog.java index 5ddaf3893a3..1c841b5e8f4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLogImportFixesAndAddMappingsDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLogImportFixesAndAddMappingsDialog.java @@ -21,13 +21,14 @@ import com.sap.sailing.gwt.ui.shared.DeviceMappingDTO; import com.sap.sailing.gwt.ui.shared.MarkDTO; import com.sap.sailing.gwt.ui.shared.TrackFileImportDeviceIdentifierDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog; public abstract class AbstractLogImportFixesAndAddMappingsDialog extends DataEntryDialog> { TrackFileImportWidget importWidget; private TrackFileImportDeviceIdentifierTableWrapper deviceIdTable; - protected final CompetitorTableWrapper> competitorTable; - protected final MarkTableWrapper> markTable; + protected final CompetitorTableWrapper> competitorTable; + protected final MarkTableWrapper> markTable; private final StringMessages stringMessages; private final Map mappings = new HashMap<>(); @@ -56,7 +57,7 @@ public abstract class AbstractLogImportFixesAndAddMappingsDialog extends DataEnt } }); competitorTable = new CompetitorTableWrapper<>(sailingService, stringMessages, errorReporter, /* multiSelection */ false, true); - markTable = new MarkTableWrapper>( + markTable = new MarkTableWrapper>( /* multiSelection */ false, sailingService, stringMessages, errorReporter); competitorTable.getSelectionModel().addSelectionChangeHandler(new SelectionChangeEvent.Handler() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java index 52520fa7e52..2f8eb97706a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorPanel.java @@ -14,7 +14,6 @@ import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.VerticalPanel; -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.dto.CompetitorDTO; @@ -36,7 +35,7 @@ public class CompetitorPanel extends SimplePanel { private final SailingServiceAsync sailingService; private final StringMessages stringMessages; private final ErrorReporter errorReporter; - private final CompetitorTableWrapper> competitorTable; + private final CompetitorTableWrapper> competitorTable; private final RefreshableMultiSelectionModel refreshableCompetitorSelectionModel; private final String leaderboardName; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java index a9e4be72683..462b37f4718 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java @@ -15,7 +15,6 @@ import com.google.gwt.user.cellview.client.TextColumn; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.ImageResourceRenderer; import com.google.gwt.user.client.ui.Label; -import com.google.gwt.view.client.SelectionModel; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.gwt.ui.adminconsole.ColorColumn.ColorRetriever; import com.sap.sailing.gwt.ui.client.FlagImageResolver; @@ -28,7 +27,7 @@ import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; -public class CompetitorTableWrapper> extends TableWrapper { +public class CompetitorTableWrapper> extends TableWrapper { private final LabeledAbstractFilterablePanel filterField; public CompetitorTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages,ErrorReporter errorReporter, diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java index 41fcd120df5..7b9d8d400f9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java @@ -1,7 +1,6 @@ package com.sap.sailing.gwt.ui.adminconsole; import com.google.gwt.user.cellview.client.TextColumn; -import com.google.gwt.view.client.SelectionModel; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.ControlPointDTO; @@ -11,7 +10,7 @@ import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; -public class ControlPointTableWrapper> extends TableWrapper { +public class ControlPointTableWrapper> extends TableWrapper { public ControlPointTableWrapper(boolean multiSelection, SailingServiceAsync sailingService, final StringMessages stringMessages, ErrorReporter errorReporter) { super(sailingService, stringMessages, errorReporter, multiSelection, /* enablePager */ true, diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CopyCourseAndCompetitorsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CopyCourseAndCompetitorsDialog.java index 096b8f8b20b..24efd341018 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CopyCourseAndCompetitorsDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CopyCourseAndCompetitorsDialog.java @@ -7,17 +7,17 @@ import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Widget; -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.gwt.ui.adminconsole.AbstractLeaderboardConfigPanel.RaceColumnDTOAndFleetDTOWithNameBasedEquality; 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.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog; public class CopyCourseAndCompetitorsDialog extends DataEntryDialog { - private final RaceTableWrapper> + private final RaceTableWrapper> racesTable; private final CheckBox courseCheckBox; private final CheckBox competitorCheckBox; @@ -38,7 +38,7 @@ public class CopyCourseAndCompetitorsDialog extends DataEntryDialog>( + racesTable = new RaceTableWrapper>( sailingService, stringMessages, errorReporter, /* multiSelection */ true); racesTable.setSelectedLeaderboardName(leaderboardName); racesTable.getDataProvider().getList().addAll(races); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CourseManagementWidget.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CourseManagementWidget.java index 3ac01a1e044..5c402d443b9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CourseManagementWidget.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CourseManagementWidget.java @@ -16,11 +16,9 @@ import com.google.gwt.user.client.ui.HasVerticalAlignment; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.IsWidget; import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.view.client.MultiSelectionModel; import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SelectionChangeEvent.Handler; import com.google.gwt.view.client.SetSelectionModel; -import com.google.gwt.view.client.SingleSelectionModel; import com.sap.sailing.domain.common.PassingInstruction; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -30,12 +28,14 @@ import com.sap.sailing.gwt.ui.shared.MarkDTO; import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; import com.sap.sailing.gwt.ui.shared.WaypointDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog; public abstract class CourseManagementWidget implements IsWidget { - protected final MarkTableWrapper> marks; - protected final ControlPointTableWrapper> multiMarkControlPoints; - protected final WaypointTableWrapper> waypoints; + protected final MarkTableWrapper> marks; + protected final ControlPointTableWrapper> multiMarkControlPoints; + protected final WaypointTableWrapper> waypoints; protected final Grid mainPanel; @@ -67,11 +67,11 @@ public abstract class CourseManagementWidget implements IsWidget { mainPanel.setCellPadding(5); mainPanel.getRowFormatter().setVerticalAlign(0, HasVerticalAlignment.ALIGN_TOP); - waypoints = new WaypointTableWrapper>( + waypoints = new WaypointTableWrapper>( /* multiSelection */ false, sailingService, stringMessages, errorReporter); - multiMarkControlPoints = new ControlPointTableWrapper>( + multiMarkControlPoints = new ControlPointTableWrapper>( /* multiSelection */ false, sailingService, stringMessages, errorReporter); - marks = new MarkTableWrapper>( + marks = new MarkTableWrapper>( /* multiSelection */ true, sailingService, stringMessages, errorReporter); CaptionPanel waypointsPanel = new CaptionPanel(stringMessages.waypoints()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceIdentifierTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceIdentifierTableWrapper.java index a47a1923e11..2025f79b76b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceIdentifierTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceIdentifierTableWrapper.java @@ -1,13 +1,13 @@ package com.sap.sailing.gwt.ui.adminconsole; import com.google.gwt.user.cellview.client.TextColumn; -import com.google.gwt.view.client.MultiSelectionModel; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.DeviceIdentifierDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; -public class DeviceIdentifierTableWrapper extends TableWrapper> { +public class DeviceIdentifierTableWrapper extends TableWrapper> { public DeviceIdentifierTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter errorReporter) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceMappingTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceMappingTableWrapper.java index 66d654661f3..fe26f2fccea 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceMappingTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceMappingTableWrapper.java @@ -11,15 +11,15 @@ import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; import com.google.gwt.user.cellview.client.TextColumn; import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.Widget; -import com.google.gwt.view.client.SingleSelectionModel; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; import com.sap.sailing.gwt.ui.shared.DeviceMappingDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; -public class DeviceMappingTableWrapper extends TableWrapper> { +public class DeviceMappingTableWrapper extends TableWrapper> { static interface FilterChangedHandler { void onFilterChanged(List filteredList); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/GateCreationDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/GateCreationDialog.java index d7fee494249..adbc9f85ee6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/GateCreationDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/GateCreationDialog.java @@ -10,7 +10,6 @@ import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.Widget; -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.gwt.ui.client.SailingServiceAsync; @@ -18,10 +17,11 @@ import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.GateDTO; import com.sap.sailing.gwt.ui.shared.MarkDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog; public class GateCreationDialog extends DataEntryDialog { - private final MarkTableWrapper> marksWrapper; + private final MarkTableWrapper> marksWrapper; private final TextBox name; private final StringMessages stringMessages; @@ -42,7 +42,7 @@ public class GateCreationDialog extends DataEntryDialog { }, /* animationEnabled */ false, callback); this.stringMessages = stringMessages; name = createTextBox(""); - marksWrapper = new MarkTableWrapper>( + marksWrapper = new MarkTableWrapper>( /* multiSelection */ true, sailingService, stringMessages, errorReporter); marksWrapper.getDataProvider().getList().addAll(marks); marksWrapper.getSelectionModel().addSelectionChangeHandler(new Handler() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ItemToMapToDeviceSelectionPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ItemToMapToDeviceSelectionPanel.java index 688aadeefd5..1e3c7928a86 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ItemToMapToDeviceSelectionPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ItemToMapToDeviceSelectionPanel.java @@ -16,10 +16,11 @@ import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.MarkDTO; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; public class ItemToMapToDeviceSelectionPanel implements IsWidget { - private final CompetitorTableWrapper> competitorTable; - private final MarkTableWrapper> markTable; + private final CompetitorTableWrapper> competitorTable; + private final MarkTableWrapper> markTable; private MappableToDevice selected; private final VerticalPanel mainPanel; private final ErrorReporter errorReporter; @@ -34,7 +35,7 @@ public class ItemToMapToDeviceSelectionPanel implements IsWidget { this.selected = selected; this.errorReporter = errorReporter; competitorTable = new CompetitorTableWrapper<>(sailingService, stringMessages, errorReporter, /* multiSelection */ false, /* enablePager */ true); - markTable = new MarkTableWrapper>(/* multiSelection */ false, sailingService, + markTable = new MarkTableWrapper>(/* multiSelection */ false, sailingService, stringMessages, errorReporter); final SingleSelectionModel markSelectionModel = markTable.getSelectionModel(); competitorTable.getSelectionModel().addSelectionChangeHandler(new SelectionChangeEvent.Handler() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java index 2a3f50c9174..a52565138de 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java @@ -6,7 +6,6 @@ import java.util.Comparator; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.user.cellview.client.Column; import com.google.gwt.user.cellview.client.TextColumn; -import com.google.gwt.view.client.SelectionModel; import com.sap.sailing.gwt.ui.adminconsole.ColorColumn.ColorRetriever; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -17,7 +16,7 @@ import com.sap.sse.common.impl.RGBColor; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; -public class MarkTableWrapper> extends TableWrapper { +public class MarkTableWrapper> extends TableWrapper { public MarkTableWrapper(boolean multiSelection, SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter errorReporter) { super(sailingService, stringMessages, errorReporter, multiSelection, true, null /*EntityIdentityComparator for RefreshableSelectionModel*/); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java index b72c2a296b4..388adf0c6af 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java @@ -9,7 +9,6 @@ import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeHtmlUtils; import com.google.gwt.user.cellview.client.Column; import com.google.gwt.user.cellview.client.TextColumn; -import com.google.gwt.view.client.SelectionModel; import com.sap.sailing.domain.common.RegattaNameAndRaceName; import com.sap.sailing.gwt.ui.adminconsole.AbstractLeaderboardConfigPanel.RaceColumnDTOAndFleetDTOWithNameBasedEquality; import com.sap.sailing.gwt.ui.adminconsole.LeaderboardConfigPanel.AnchorCell; @@ -21,7 +20,7 @@ import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; -public class RaceTableWrapper> +public class RaceTableWrapper> extends TableWrapper { private final AnchorTemplates ANCHORTEMPLATE = GWT.create(AnchorTemplates.class); private String selectedLeaderboardName; @@ -75,8 +74,4 @@ extends TableWrapper { public void setSelectedLeaderboardName(String name) { this.selectedLeaderboardName = name; } - - public RefreshableSelectionModel getRefreshableSelectionModel() { - return (RefreshableSelectionModel) super.getSelectionModel(); - } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaLogTrackingDeviceMappingsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaLogTrackingDeviceMappingsDialog.java index 86672aeb74a..be18983bff0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaLogTrackingDeviceMappingsDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaLogTrackingDeviceMappingsDialog.java @@ -29,7 +29,6 @@ public class RegattaLogTrackingDeviceMappingsDialog extends AbstractLogTrackingD @Override public void onSuccess(List result) { mappings = result; - updateChart(); deviceMappingTable.refresh(mappings); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java index cb1024fe7eb..5edfb750251 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java @@ -8,13 +8,13 @@ import com.google.gwt.user.client.ui.IsWidget; 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.SelectionModel; 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.SelectionCheckboxColumn; import com.sap.sse.common.Util; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; /** @@ -22,8 +22,7 @@ import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; * offers already a {@link ListHandler} for sorting. Subclasses can obtain the table's default column sort handler * created by this class's constructor by calling {@link #getColumnSortHandler}. */ -//TODO change > to > -public abstract class TableWrapper> implements IsWidget { +public abstract class TableWrapper> implements IsWidget { protected final CellTable table; private final S selectionModel; protected final ListDataProvider dataProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointCreationDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointCreationDialog.java index 42b98c2bc79..e103e0cae81 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointCreationDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointCreationDialog.java @@ -9,7 +9,6 @@ import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.Widget; 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.PassingInstruction; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -17,10 +16,11 @@ import com.sap.sailing.gwt.ui.shared.ControlPointDTO; import com.sap.sailing.gwt.ui.shared.WaypointDTO; import com.sap.sse.common.Util; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog; public class WaypointCreationDialog extends DataEntryDialog { - private final ControlPointTableWrapper> controlPointsWrapper; + private final ControlPointTableWrapper> controlPointsWrapper; private final ListBox passingInstructions; private final StringMessages stringMessages; @@ -39,7 +39,7 @@ public class WaypointCreationDialog extends DataEntryDialog { }, /* animationEnabled */ false, callback); this.stringMessages = stringMessages; - controlPointsWrapper = new ControlPointTableWrapper>( + controlPointsWrapper = new ControlPointTableWrapper>( /* multiSelection */ false, sailingService, stringMessages, errorReporter); controlPointsWrapper.getDataProvider().getList().addAll(controlPoints); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java index dc3a9588c0c..dbd7cd7093b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java @@ -1,7 +1,6 @@ package com.sap.sailing.gwt.ui.adminconsole; import com.google.gwt.user.cellview.client.TextColumn; -import com.google.gwt.view.client.SelectionModel; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.ControlPointDTO; @@ -11,7 +10,7 @@ import com.sap.sailing.gwt.ui.shared.WaypointDTO; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; -public class WaypointTableWrapper> extends TableWrapper { +public class WaypointTableWrapper> extends TableWrapper { public WaypointTableWrapper(boolean multiSelection, SailingServiceAsync sailingService, final StringMessages stringMessages, ErrorReporter errorReporter) { super(sailingService, stringMessages, errorReporter, multiSelection, true, null /*EntityIdentityComparator for RefreshableSelectionModel*/); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java index fa6475f0dad..1315019f7cc 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java @@ -52,7 +52,7 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW private final RefreshableMultiSelectionModel selectionModel; /** - * @param selectionmodel + * @param selectionModel * {@link RefreshableSelectionModel} to set an already existing selectionmodel for the * {@link SelectionCheckboxColumn} * @param selectedCheckboxCSSClass @@ -62,9 +62,10 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW * @param checkboxColumnCellCSSClass * CSS class for the <td> element rendering the cell */ - protected SelectionCheckboxColumn(RefreshableMultiSelectionModel selectionmodel, String selectedCheckboxCSSClass, + // TODO Lukas: test whether using an arbitrary selection model here that does not trigger the redrawRow method displays the checkbox correctly when the selection is changed programmatically; is this required at all, or could the client grab the selection model from this column instead? + protected SelectionCheckboxColumn(RefreshableMultiSelectionModel selectionModel, String selectedCheckboxCSSClass, String deselectedCheckboxCSSClass, String checkboxColumnCellCSSClass) { - this(selectionmodel, new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass); + this(selectionModel, new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass); } /** * @param selectedCheckboxCSSClass @@ -82,6 +83,7 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW entityIdentityComparator); } + // TODO Lukas: test whether using an arbitrary selection model here that does not trigger the redrawRow method displays the checkbox correctly when the selection is changed programmatically; is this required at all, or could the client grab the selection model from this column instead? private SelectionCheckboxColumn(RefreshableMultiSelectionModel selectionModel, BetterCheckboxCell checkboxCell, String checkboxColumnCellCSSClass) { super(checkboxCell, SortingOrder.DESCENDING); this.cell = checkboxCell; @@ -143,7 +145,6 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW redrawRow(item, getListDataProvider().getList()); } } - }; } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 1d88541c66e..1da25bb83e7 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -6,6 +6,11 @@ import com.google.gwt.view.client.MultiSelectionModel; import com.google.gwt.view.client.ProvidesKey; import com.google.gwt.view.client.SelectionChangeEvent; +/** + * TODO Lukas: Add Javadoc + * + * @param + */ public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel { private final EntityIdentityComparator comp; @@ -14,11 +19,17 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im comp = null; } + /** + * TODO Lukas: Add Javadoc + */ public RefreshableMultiSelectionModel(EntityIdentityComparator comp) { super(); this.comp = comp; } + /** + * TODO Lukas: Add Javadoc + */ public RefreshableMultiSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { super(keyProvider); this.comp = comp; From e48b43dbec51657552895ef1c48f5ac8140bf58b Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 2 Dec 2015 10:31:04 +0100 Subject: [PATCH 079/306] applied pattern for selection change handler consistently in AbstractRaceManagementPanel --- .../AbstractRaceManagementPanel.java | 48 ++++++++----------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java index 4651e402fcd..283b8d9196b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRaceManagementPanel.java @@ -29,27 +29,38 @@ public abstract class AbstractRaceManagementPanel extends AbstractEventManagemen public AbstractRaceManagementPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, RegattaRefresher regattaRefresher, boolean actionButtonsEnabled, StringMessages stringMessages) { super(sailingService, regattaRefresher, errorReporter, actionButtonsEnabled, stringMessages); - VerticalPanel mainPanel = new VerticalPanel(); this.setWidget(mainPanel); mainPanel.setWidth("100%"); - mainPanel.add(trackedRacesListComposite); - trackedRacesListComposite.getSelectionModel().addSelectionChangeHandler(new Handler() { - @Override public void onSelectionChange(SelectionChangeEvent event) { - onRaceSelectionChange(trackedRacesListComposite.getSelectionModel().getSelectedSet()); + Set selectedRaces = trackedRacesListComposite.getSelectionModel().getSelectedSet(); + if (selectedRaces.size() == 1) { + singleSelectedRace = selectedRaces.iterator().next().getRaceIdentifier(); + selectedCaptionRacePanel.setCaptionText(singleSelectedRace.getRaceName()); + selectedCaptionRacePanel.setVisible(true); + for (RegattaDTO regatta : getAvailableRegattas()) { + for (RaceDTO race : regatta.races) { + if (race != null && race.getRaceIdentifier().equals(singleSelectedRace)) { + AbstractRaceManagementPanel.this.selectedRaceDTO = race; + refreshSelectedRaceData(); + break; + } + } + } + } else { + selectedCaptionRacePanel.setCaptionText(""); + singleSelectedRace = null; + selectedCaptionRacePanel.setVisible(false); + } } }); - singleSelectedRace = null; - selectedCaptionRacePanel = new CaptionPanel(stringMessages.race()); selectedCaptionRacePanel.setWidth("100%"); mainPanel.add(selectedCaptionRacePanel); - selectedRaceContentPanel = new VerticalPanel(); selectedRaceContentPanel.setWidth("100%"); selectedCaptionRacePanel.setContentWidget(selectedRaceContentPanel); @@ -57,25 +68,4 @@ public abstract class AbstractRaceManagementPanel extends AbstractEventManagemen } abstract void refreshSelectedRaceData(); - - public void onRaceSelectionChange(Set selectedRaces) { - if (selectedRaces.size() == 1) { - singleSelectedRace = selectedRaces.iterator().next().getRaceIdentifier(); - selectedCaptionRacePanel.setCaptionText(singleSelectedRace.getRaceName()); - selectedCaptionRacePanel.setVisible(true); - for (RegattaDTO regatta : getAvailableRegattas()) { - for (RaceDTO race : regatta.races) { - if (race != null && race.getRaceIdentifier().equals(singleSelectedRace)) { - this.selectedRaceDTO = race; - refreshSelectedRaceData(); - break; - } - } - } - } else { - selectedCaptionRacePanel.setCaptionText(""); - singleSelectedRace = null; - selectedCaptionRacePanel.setVisible(false); - } - } } From 2bf3755fc2b155580a199732ce3926a5a521f268 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 2 Dec 2015 11:00:02 +0100 Subject: [PATCH 080/306] removed use of RaceSelectionProvider in RibDashboardDataRetriever --- .../RibDashboardDataRetriever.java | 53 +------------------ 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java index 7ef3fbd6c27..fe206005f0a 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java @@ -1,7 +1,6 @@ package com.sap.sailing.dashboards.gwt.client.dataretriever; import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.List; import java.util.logging.Level; @@ -12,9 +11,6 @@ import com.google.gwt.user.client.rpc.AsyncCallback; import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; import com.sap.sailing.dashboards.gwt.client.actions.GetRibDashboardRaceInfoAction; import com.sap.sailing.dashboards.gwt.shared.dto.RibDashboardRaceInfoDTO; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.player.TimeListener; @@ -26,11 +22,10 @@ import com.sap.sse.gwt.client.player.TimeListener; * @author Alexander Ries * */ -public class RibDashboardDataRetriever implements TimeListener, RaceSelectionProvider { +public class RibDashboardDataRetriever implements TimeListener { private String leaderboardName; private ArrayList dataRetrieverListener; - private ArrayList raceSelectionChangeListener; private AsyncActionsExecutor asyncActionsExecutor; @@ -51,7 +46,6 @@ public class RibDashboardDataRetriever implements TimeListener, RaceSelectionPro private void initNonFinalMemberVariablesWithNoArgumentConstructor() { dataRetrieverListener = new ArrayList(); - raceSelectionChangeListener = new ArrayList(); } private void loadLiveRaceInfoFromRibDashboadService() { @@ -66,10 +60,8 @@ public class RibDashboardDataRetriever implements TimeListener, RaceSelectionPro switch (result.responseMessage) { case RACE_LIVE: logger.log(Level.INFO, "RibDashboardRaceInfoDTO.responseMessage is RACE_LIVE"); - if(result.idOfLastTrackedRace != null) { + if (result.idOfLastTrackedRace != null) { logger.log(Level.INFO, "RibDashboardRaceInfoDTO idOfLastTrackedRace is "+result.idOfLastTrackedRace); - List singletonList = Collections.singletonList(result.idOfLastTrackedRace); - setSelection(singletonList); } else { logger.log(Level.INFO, "RibDashboardRaceInfoDTO.idOfLastTrackedRace is null"); } @@ -123,45 +115,4 @@ public class RibDashboardDataRetriever implements TimeListener, RaceSelectionPro public void timeChanged(Date newTime, Date oldTime) { loadLiveRaceInfoFromRibDashboadService(); } - - @Override - public void addRaceSelectionChangeListener(RaceSelectionChangeListener listener) { - synchronized (MUTEX) { - if (listener != null) { - raceSelectionChangeListener.add(listener); - } - } - } - - @Override - public void removeRaceSelectionChangeListener(RaceSelectionChangeListener listener) { - synchronized (MUTEX) { - raceSelectionChangeListener.remove(listener); - } - } - - @Override - public void setSelection(List newSelection, - RaceSelectionChangeListener... listenersNotToNotify) { - synchronized (MUTEX) { - for (RaceSelectionChangeListener currentRaceSelectionChangeListener : raceSelectionChangeListener) { - currentRaceSelectionChangeListener.onRaceSelectionChange(newSelection); - } - } - } - - @Override - public List getAllRaces() { - return null; - } - - @Override - public void setAllRaces(List newAllRaces, - RaceSelectionChangeListener... listenersNotToNotify) { - } - - @Override - public List getSelectedRaces() { - return null; - } } From 97e12fee7973b53c51af96c6ca5f5362c42b3375 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 2 Dec 2015 11:27:25 +0100 Subject: [PATCH 081/306] removed RaceSelectionProvider and RaceSelectionModel; pass RegattaAndRaceIdentifier directly instead --- .../startanalysis/StartlineAnalysisCard.java | 1 - .../place/player/AutoPlayController.java | 9 +- .../ui/adminconsole/RacesListBoxPanel.java | 152 ----------------- .../client/RaceSelectionChangeListener.java | 23 --- .../gwt/ui/client/RaceSelectionModel.java | 90 ---------- .../gwt/ui/client/RaceSelectionProvider.java | 25 --- .../sailing/gwt/ui/client/RaceTimePanel.java | 18 +- .../gwt/ui/client/SmallWindHistoryPanel.java | 159 ------------------ .../charts/AbstractCompetitorRaceChart.java | 28 +-- .../shared/charts/AbstractRaceChart.java | 4 +- .../shared/charts/EditMarkPassingsPanel.java | 11 +- .../charts/MultiCompetitorRaceChart.java | 6 +- .../ui/client/shared/charts/WindChart.java | 33 ++-- .../gwt/ui/client/shared/racemap/RaceMap.java | 24 +-- .../EmbeddedMapAndWindChartEntryPoint.java | 10 +- .../gwt/ui/raceboard/RaceBoardEntryPoint.java | 11 +- .../gwt/ui/raceboard/RaceBoardPanel.java | 27 +-- 17 files changed, 50 insertions(+), 581 deletions(-) delete mode 100755 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RacesListBoxPanel.java delete mode 100755 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionChangeListener.java delete mode 100755 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionModel.java delete mode 100755 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionProvider.java delete mode 100755 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SmallWindHistoryPanel.java diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisCard.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisCard.java index 60ebd904b53..3d4bc02cbec 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisCard.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisCard.java @@ -153,7 +153,6 @@ public class StartlineAnalysisCard extends Composite implements HasWidgets, Star raceMap = new RaceMap(sailingServiceAsync, asyncActionsExecutor, null, timer, competitorSelectionModel, StringMessages.INSTANCE, false, false, false, false, startAnalysisDTO.regattaAndRaceIdentifier, resources.combinedWindPanelStyle(), /* showHeaderPanel */ true); - raceMap.onRaceSelectionChange(Collections.singletonList(startAnalysisDTO.regattaAndRaceIdentifier)); raceMap.getSettings().setZoomSettings(new RaceMapZoomSettings(zoomTypes, false)); raceMap.getSettings().setHelpLinesSettings(getHelpLineSettings()); raceMap.getSettings().setTailLengthInMilliseconds(startAnalysisDTO.tailLenghtInMilliseconds); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java index 5cfe4fe6697..bc2d5d95a8e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/place/player/AutoPlayController.java @@ -1,9 +1,7 @@ package com.sap.sailing.gwt.autoplay.client.place.player; import java.util.ArrayList; -import java.util.Collections; import java.util.Date; -import java.util.List; import java.util.Map; import com.google.gwt.core.client.Scheduler; @@ -29,7 +27,6 @@ import com.sap.sailing.gwt.common.client.FullscreenUtil; import com.sap.sailing.gwt.common.client.i18n.TextMessages; import com.sap.sailing.gwt.ui.client.CompetitorSelectionModel; import com.sap.sailing.gwt.ui.client.MediaServiceAsync; -import com.sap.sailing.gwt.ui.client.RaceSelectionModel; import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -167,12 +164,8 @@ public class AutoPlayController implements RaceTimesInfoProviderListener { } private RaceBoardPanel createRaceBoardPanel(String leaderboardName, RegattaAndRaceIdentifier raceToShow, Map competitorsAndTheirBoats) { - RaceSelectionModel raceSelectionModel = new RaceSelectionModel(); - List singletonList = Collections.singletonList(raceToShow); - raceSelectionModel.setSelection(singletonList); - RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, userService, asyncActionsExecutor, - competitorsAndTheirBoats, raceboardTimer, raceSelectionModel, leaderboardName, null, /* event */null, raceboardViewConfig, + competitorsAndTheirBoats, raceboardTimer, raceToShow, leaderboardName, null, /* event */null, raceboardViewConfig, errorReporter, StringMessages.INSTANCE, userAgent, raceTimesInfoProvider, /* showMapControls */false); return raceBoardPanel; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RacesListBoxPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RacesListBoxPanel.java deleted file mode 100755 index 706b564ebef..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RacesListBoxPanel.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.sap.sailing.gwt.ui.adminconsole; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -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.user.client.ui.Button; -import com.google.gwt.user.client.ui.FormPanel; -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.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.dto.RaceDTO; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; -import com.sap.sailing.gwt.ui.client.RegattaRefresher; -import com.sap.sailing.gwt.ui.client.RegattasDisplayer; -import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.shared.RegattaDTO; - -public class RacesListBoxPanel extends FormPanel implements RegattasDisplayer, RaceSelectionChangeListener { - private final List raceList; - private final ListBox raceListBox; - private final RegattaRefresher regattaRefresher; - private final StringMessages stringConstants; - private final RaceSelectionProvider raceSelectionProvider; - - public RacesListBoxPanel(RegattaRefresher regattaRefresher, RaceSelectionProvider raceSelectionProvider, StringMessages stringConstants) { - this.regattaRefresher = regattaRefresher; - this.stringConstants = stringConstants; - this.raceSelectionProvider = raceSelectionProvider; - raceListBox = new ListBox(); - raceListBox.addChangeHandler(new ChangeHandler() { - @Override - public void onChange(ChangeEvent event) { - RacesListBoxPanel.this.raceSelectionProvider.setSelection(getSelectedRaceIdentifiers(), RacesListBoxPanel.this); - } - }); - raceListBox.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - RacesListBoxPanel.this.raceSelectionProvider.setSelection(getSelectedRaceIdentifiers(), RacesListBoxPanel.this); - } - }); - raceList = new ArrayList(); - VerticalPanel vp = new VerticalPanel(); - HorizontalPanel labelAndRefreshButton = new HorizontalPanel(); - labelAndRefreshButton.setSpacing(20); - vp.add(labelAndRefreshButton); - labelAndRefreshButton.add(new Label(stringConstants.races())); - vp.add(raceListBox); - Button btnRefresh = new Button(stringConstants.refresh()); - btnRefresh.addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - RacesListBoxPanel.this.regattaRefresher.fillRegattas(); - } - }); - labelAndRefreshButton.add(btnRefresh); - setWidget(vp); - } - - @Override - public void fillRegattas(Iterable result) { - raceList.clear(); - raceListBox.clear(); - for (RegattaDTO regatta : result) { - for (RaceDTO race : regatta.races) { - raceList.add(race); - } - } - Collections.sort(raceList, new Comparator() { - @Override - public int compare(RaceDTO o1, RaceDTO o2) { - String name1 = RacesListBoxPanel.this.toString(o1); - String name2 = RacesListBoxPanel.this.toString(o2); - return name1.compareTo(name2); - } - - }); - for (RaceDTO p : raceList) { - raceListBox.addItem(toString(p)); - } - raceSelectionProvider.setSelection(getSelectedRaceIdentifiers(), this); - } - - private List getSelectedRaceIdentifiers() { - List result = new ArrayList(); - for (RaceDTO selectedRace : getSelectedRaces()) { - result.add(selectedRace.getRaceIdentifier()); - } - return result; - } - - private List getSelectedRaces() { - int i=0; - List result = new ArrayList(); - for (RaceDTO triple : raceList) { - if (raceListBox.isItemSelected(i)) { - result.add(triple); - } - i++; - } - return result; - } - - private String toString(RaceDTO race) { - return race.getRegattaName()+" - "+race.getName()+(race.isTracked ? " ("+stringConstants.tracked()+")" : ""); - } - - @Override - public void onRaceSelectionChange(List selectedRaces) { - if (selectedRaces != null && !selectedRaces.isEmpty()) { - RaceDTO newSelection = getRace(selectedRaces.iterator().next()); - int index = raceList.indexOf(newSelection); - if (index >= 0) { - raceListBox.setSelectedIndex(index); - } else { - deselectAll(); - } - } else { - deselectAll(); - } - } - - private void deselectAll() { - for (int i=0; i getAllRaces() { - return Collections.unmodifiableList(raceList); - } - - public RaceDTO getRace(RaceIdentifier raceIdentifier) { - for (RaceDTO race : getAllRaces()) { - if (raceIdentifier.equals(race.getRaceIdentifier())) { - return race; - } - } - return null; - } - -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionChangeListener.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionChangeListener.java deleted file mode 100755 index de8556b6dc5..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionChangeListener.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.List; - -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; - - -/** - * Allows UI components to observe a race selector, such as a drop-down box showing a list of races - * - * @author Axel Uhl (d043530) - * - */ -public interface RaceSelectionChangeListener { - - /** - * The first element is the first one selected - * - * @param selectedRaces a non-null list which is empty if nothing is selected - */ - void onRaceSelectionChange(List selectedRaces); - -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionModel.java deleted file mode 100755 index 556bbf02538..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionModel.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; - -public class RaceSelectionModel implements RaceSelectionProvider { - private final List selection; - - private final List allRaces; - - private final Set listeners; - - public RaceSelectionModel() { - this.selection = new ArrayList(); - this.allRaces = new ArrayList(); - listeners = new HashSet(); - } - - @Override - public List getSelectedRaces() { - return Collections.unmodifiableList(selection); - } - - @Override - public void addRaceSelectionChangeListener(RaceSelectionChangeListener listener) { - listeners.add(listener); - } - - @Override - public void removeRaceSelectionChangeListener(RaceSelectionChangeListener listener) { - listeners.remove(listener); - } - - @Override - public void setSelection(List newSelection, RaceSelectionChangeListener... listenersNotToNotify) { - boolean notify = !selection.equals(newSelection); - selection.clear(); - selection.addAll(newSelection); - if (notify) { - notifyListeners(listenersNotToNotify); - } - } - - private void notifyListeners(RaceSelectionChangeListener[] listenersNotToNotify) { - List selectedRaces = getSelectedRaces(); - for (RaceSelectionChangeListener listener : listeners) { - if (listenersNotToNotify == null || !Arrays.asList(listenersNotToNotify).contains(listener)) { - listener.onRaceSelectionChange(selectedRaces); - } - } - } - - /** - * Sets the universe of all races from which this selection model may select. An equal list will subsequently be - * returned by {@link #getAllRaces()}. Races from the {@link #getSelectedRaces() selection} not in - * newAllRaces are removed from the selection. If this happens, the selection listeners are notified. - */ - @Override - public void setAllRaces(List newAllRaces, RaceSelectionChangeListener... listenersNotToNotify) { - allRaces.clear(); - for (RegattaAndRaceIdentifier r : newAllRaces) { - allRaces.add(r); - } - boolean notify = false; - for (Iterator i=selection.iterator(); i.hasNext(); ) { - RaceIdentifier selectedRace = i.next(); - if (!allRaces.contains(selectedRace)) { - notify = true; - i.remove(); - } - } - if (notify) { - notifyListeners(listenersNotToNotify); - } - } - - @Override - public List getAllRaces() { - return Collections.unmodifiableList(allRaces); - } - -} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionProvider.java deleted file mode 100755 index 5db6435e36b..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceSelectionProvider.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.List; - -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; - -public interface RaceSelectionProvider { - /** - * @return a non-null list which may be empty - */ - List getSelectedRaces(); - - void addRaceSelectionChangeListener(RaceSelectionChangeListener listener); - - void removeRaceSelectionChangeListener(RaceSelectionChangeListener listener); - - void setSelection(List newSelection, RaceSelectionChangeListener... listenersNotToNotify); - - /** - * @return a non-null list which may be empty - */ - List getAllRaces(); - - void setAllRaces(List newAllRaces, RaceSelectionChangeListener... listenersNotToNotify); -} 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 ef6d167bb08..85ac2790950 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 @@ -15,7 +15,7 @@ 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.SettingsDialogComponent; -public class RaceTimePanel extends TimePanel implements RaceSelectionChangeListener, RaceTimesInfoProviderListener { +public class RaceTimePanel extends TimePanel implements RaceTimesInfoProviderListener { private final RaceTimesInfoProvider raceTimesInfoProvider; private RegattaAndRaceIdentifier selectedRace; @@ -30,11 +30,15 @@ public class RaceTimePanel extends TimePanel implements R private boolean redrawAllMarkersPendingForMinMaxBeingInitialized; public RaceTimePanel(Timer timer, TimeRangeWithZoomProvider timeRangeProvider, StringMessages stringMessages, - RaceTimesInfoProvider raceTimesInfoProvider, boolean canReplayWhileLiveIsPossible) { + RaceTimesInfoProvider raceTimesInfoProvider, boolean canReplayWhileLiveIsPossible, RegattaAndRaceIdentifier selectedRaceIdentifier) { super(timer, timeRangeProvider, stringMessages, canReplayWhileLiveIsPossible); this.raceTimesInfoProvider = raceTimesInfoProvider; selectedRace = null; autoAdjustPlayMode = true; + selectedRace = selectedRaceIdentifier; + if (!raceTimesInfoProvider.containsRaceIdentifier(selectedRace)) { + raceTimesInfoProvider.addRaceIdentifier(selectedRace, true); + } } /** @@ -148,16 +152,6 @@ public class RaceTimePanel extends TimePanel implements R (lastRaceTimesInfo.startOfRace != null && liveTimePointInMillis > lastRaceTimesInfo.startOfRace.getTime() - timeBeforeRaceStartTolerance)); } - @Override - public void onRaceSelectionChange(List selectedRaces) { - if (selectedRaces != null && !selectedRaces.isEmpty()) { - selectedRace = selectedRaces.iterator().next(); - if (!raceTimesInfoProvider.containsRaceIdentifier(selectedRace)) { - raceTimesInfoProvider.addRaceIdentifier(selectedRace, true); - } - } - } - @Override public void playStateChanged(PlayStates playState, PlayModes playMode) { super.playStateChanged(playState, playMode); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SmallWindHistoryPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SmallWindHistoryPanel.java deleted file mode 100755 index f5d3b5d2fe4..00000000000 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SmallWindHistoryPanel.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.sap.sailing.gwt.ui.client; - -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.FormPanel; -import com.google.gwt.user.client.ui.Grid; -import com.google.gwt.user.client.ui.Label; -import com.google.gwt.user.client.ui.VerticalPanel; -import com.sap.sailing.domain.common.Position; -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.gwt.ui.shared.WindDTO; -import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; -import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; -import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.player.TimeListener; - -/** - * Displays a bit of wind history around the time notified to this time listener. - * - * @author Axel Uhl (d043530) - * - */ -public class SmallWindHistoryPanel extends FormPanel implements TimeListener, RaceSelectionChangeListener { - private final Position position; - private final WindIndicator[] windIndicators; - private final Label selectedWindSourceLabel; - private Date date; - private RegattaAndRaceIdentifier race; - private final SailingServiceAsync sailingService; - private final long millisecondStepsPerLabel; - private final ErrorReporter errorReporter; - private final StringMessages stringConstants; - - /** - * @param position - * the position to show the wind for; can, e.g., be the middle of the race course if an average wind - * display for a race is desired - * @param numberOfTimepoints - * the number of wind displays arranged in this panel; each display shows the estimated wind - * @param windTrack - * the wind track to visualize using this panel - */ - public SmallWindHistoryPanel(SailingServiceAsync sailingService, Position position, - int numberOfTimepoints, long millisecondStepsPerLabel, StringMessages stringConstants, ErrorReporter errorReporter) { - this.sailingService = sailingService; - this.position = position; - this.stringConstants = stringConstants; - this.millisecondStepsPerLabel = millisecondStepsPerLabel; - this.errorReporter = errorReporter; - windIndicators = new WindIndicator[numberOfTimepoints]; - VerticalPanel vp = new VerticalPanel(); - setWidget(vp); - Grid grid = new Grid(2, numberOfTimepoints); - selectedWindSourceLabel = new Label(); - vp.add(selectedWindSourceLabel); - for (int i=0; i() { - @Override - public void onSuccess(WindInfoForRaceDTO result) { - if (result != null) { - WindSource bestWindSource = getBestWindSource(result, windIndicators.length); - // expecting to find windIndicators.length fixes - if (result == null - || result.windTrackInfoByWindSource.get(bestWindSource).windFixes.size() != windIndicators.length) { - clearWindDisplay(); - } else { - setSelectedWindSource(bestWindSource); - int i = 0; - for (WindDTO fix : result.windTrackInfoByWindSource.get(bestWindSource).windFixes) { - updateWindIndicator(i, fix); - i++; - } - } - } - } - - @Override - public void onFailure(Throwable caught) { - errorReporter.reportError("Error trying to obtain wind information for race " - + race + ": " + caught.getMessage()); - } - }); - } - } - } - - /** - * Looks for the best wind source in windInfo that has length fixes. Uses the order of - * literals of {@link WindSourceType} to determine precedence. - * - * @return null if no wind source has the expected number of fixes; the best wind source with the - * expected number of fixes otherwise - */ - private WindSource getBestWindSource(WindInfoForRaceDTO windInfo, int expectedNumberOfFixes) { - List windSourceTypesInOrder = Arrays.asList(WindSourceType.values()); - WindSource result = null; - int bestIndexSoFar = Integer.MAX_VALUE; - for (Map.Entry e : windInfo.windTrackInfoByWindSource.entrySet()) { - if (e.getValue().windFixes.size() == expectedNumberOfFixes) { - int index = windSourceTypesInOrder.indexOf(e.getKey()); - if (index < bestIndexSoFar) { - bestIndexSoFar = index; - result = e.getKey(); - } - } - } - return result; - } - - private void clearWindDisplay() { - for (int i=0; i selectedRaces) { - if (!selectedRaces.isEmpty()) { - race = selectedRaces.get(selectedRaces.size() - 1); - updateWindDisplay(); - } - } - -} 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 58bfe7fe2aa..66cb9d0bd54 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 @@ -45,7 +45,6 @@ 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.RaceSelectionProvider; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.CompetitorRaceDataDTO; @@ -94,16 +93,16 @@ public abstract class AbstractCompetitorRaceChart selectedRaces) { - if (selectedRaces != null && !selectedRaces.isEmpty()) { - selectedRaceIdentifier = selectedRaces.iterator().next(); - } else { - selectedRaceIdentifier = null; - } - - clearChart(); - if (selectedRaceIdentifier != null) { - timeChanged(timer.getTime(), null); - } - } - /** * Checks the relation of the mark passings to the selection range. * 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 64e2ddfae51..9ad391e7444 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 @@ -18,7 +18,6 @@ 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.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; 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; @@ -33,8 +32,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.SettingsDialog; -public abstract class AbstractRaceChart extends AbsolutePanel implements RaceSelectionChangeListener, - TimeListener, TimeZoomChangeListener, TimeRangeChangeListener { +public abstract class AbstractRaceChart extends AbsolutePanel 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. 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 a812cae69a8..6e060aec45b 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 @@ -32,7 +32,6 @@ import com.sap.sailing.domain.common.dto.LeaderboardDTO; import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.gwt.ui.client.CompetitorSelectionChangeListener; import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.RaceCourseDTO; @@ -47,8 +46,7 @@ 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; -public class EditMarkPassingsPanel extends AbsolutePanel implements Component, RaceSelectionChangeListener, - CompetitorSelectionChangeListener { +public class EditMarkPassingsPanel extends AbsolutePanel implements Component, CompetitorSelectionChangeListener { private static class AnchorCell extends AbstractCell { @Override public void render(com.google.gwt.cell.client.Cell.Context context, SafeHtml safeHtml, SafeHtmlBuilder sb) { @@ -259,6 +257,7 @@ public class EditMarkPassingsPanel extends AbsolutePanel implements Component selectedRaces) { - raceIdentifier = selectedRaces.iterator().next(); - refreshWaypoints(); - } - public void setLeaderboard(LeaderboardDTO leaderboard) { this.leaderboard = leaderboard; } 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 faf11f93c11..3848f3d2cf3 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 @@ -3,8 +3,8 @@ 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.RaceSelectionProvider; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; @@ -33,11 +33,11 @@ public class MultiCompetitorRaceChart extends AbstractCompetitorRaceChart colorMap; /** - * @param raceSelectionProvider + * @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(SailingServiceAsync sailingService, RaceSelectionProvider raceSelectionProvider, Timer timer, + public WindChart(SailingServiceAsync sailingService, RegattaAndRaceIdentifier selectedRaceIdentifier, Timer timer, TimeRangeWithZoomProvider timeRangeWithZoomProvider, WindChartSettings settings, final StringMessages stringMessages, AsyncActionsExecutor asyncActionsExecutor, ErrorReporter errorReporter, boolean compactChart) { super(sailingService, timer, timeRangeWithZoomProvider, stringMessages, asyncActionsExecutor, errorReporter); @@ -190,7 +188,15 @@ public class WindChart extends AbstractRaceChart implements Component selectedRaces) { - if (selectedRaces != null && !selectedRaces.isEmpty()) { - selectedRaceIdentifier = selectedRaces.iterator().next(); - } else { - selectedRaceIdentifier = null; - } - - if(selectedRaceIdentifier != null) { - clearCacheAndReload(); - if(isVisible()) - updateVisibleSeries(); - } else { - clearChart(); - } - } - /** * 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, 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 b4657f0d566..81995e7c360 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 @@ -90,7 +90,6 @@ 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.NumberFormatterFactory; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; import com.sap.sailing.gwt.ui.client.RaceTimesInfoProviderListener; import com.sap.sailing.gwt.ui.client.RequiresDataInitialization; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -137,7 +136,7 @@ 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; -public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSelectionChangeListener, RaceSelectionChangeListener, +public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSelectionChangeListener, RaceTimesInfoProviderListener, TailFactory, Component, RequiresDataInitialization, RequiresResize, QuickRankProvider { public static final String GET_RACE_MAP_DATA_CATEGORY = "getRaceMapData"; public static final String GET_WIND_DATA_CATEGORY = "getWindData"; @@ -286,8 +285,6 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe private final CompetitorSelectionProvider competitorSelection; - private List selectedRaces; - /** * Used to check if the first initial zoom to the mark markers was already done. */ @@ -441,6 +438,8 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe trueNorthIndicatorPanel = new TrueNorthIndicatorPanel(this, raceMapImageManager, combinedWindPanelStyle, stringMessages, coordinateSystem); trueNorthIndicatorPanel.setVisible(true); orientationChangeInProgress = false; + mapFirstZoomDone = false; + // TODO bug 494: reset zoom settings to user preferences } /** @@ -735,17 +734,10 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe return panelForRightHeaderLabels; } - @Override - public void onRaceSelectionChange(List selectedRaces) { - mapFirstZoomDone = false; - // TODO bug 494: reset zoom settings to user preferences - this.selectedRaces = selectedRaces; - } - @Override public void raceTimesInfosReceived(Map raceTimesInfos, long clientTimeWhenRequestWasSent, Date serverTimeDuringRequest, long clientTimeWhenResponseWasReceived) { timer.adjustClientServerOffset(clientTimeWhenRequestWasSent, serverTimeDuringRequest, clientTimeWhenResponseWasReceived); - this.lastRaceTimesInfo = raceTimesInfos.get(selectedRaces.get(0)); + this.lastRaceTimesInfo = raceTimesInfos.get(raceIdentifier); } /** @@ -761,8 +753,8 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe @Override public void timeChanged(final Date newTime, final Date oldTime) { if (newTime != null && isMapInitialized) { - if (selectedRaces != null && !selectedRaces.isEmpty()) { - RegattaAndRaceIdentifier race = selectedRaces.get(selectedRaces.size() - 1); + if (raceIdentifier != null) { + RegattaAndRaceIdentifier race = raceIdentifier; final Iterable competitorsToShow = getCompetitorsToShow(); if (race != null) { @@ -1941,8 +1933,8 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe vPanel.add(createInfoWindowLabelAndValue(stringMessages.degreesBoatToTheWind(), (int) Math.abs(lastFix.degreesBoatToTheWind) + " " + stringMessages.degreesShort())); } - if (!selectedRaces.isEmpty()) { - RegattaAndRaceIdentifier race = selectedRaces.get(selectedRaces.size() - 1); + if (raceIdentifier != null) { + RegattaAndRaceIdentifier race = raceIdentifier; if (race != null) { Map from = new HashMap(); from.put(competitorDTO, fixesAndTails.getFixes(competitorDTO).get(fixesAndTails.getFirstShownFix(competitorDTO)).timepoint); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java index 926e3ca3efe..f72936ca077 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/EmbeddedMapAndWindChartEntryPoint.java @@ -2,9 +2,7 @@ package com.sap.sailing.gwt.ui.raceboard; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; @@ -29,7 +27,6 @@ import com.sap.sailing.gwt.ui.client.CompetitorColorProviderImpl; import com.sap.sailing.gwt.ui.client.CompetitorSelectionModel; import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; import com.sap.sailing.gwt.ui.client.LogoAndTitlePanel; -import com.sap.sailing.gwt.ui.client.RaceSelectionModel; import com.sap.sailing.gwt.ui.client.TimePanel; import com.sap.sailing.gwt.ui.client.TimePanelSettings; import com.sap.sailing.gwt.ui.client.shared.charts.WindChart; @@ -141,9 +138,6 @@ public class EmbeddedMapAndWindChartEntryPoint extends AbstractSailingEntryPoint title.append(fleetName); } Window.setTitle(title.toString()); - final RaceSelectionModel raceSelectionModel = new RaceSelectionModel(); - final List raceList = Collections.singletonList(selectedRaceIdentifier); - raceSelectionModel.setSelection(raceList); final long refreshInterval = Duration.ONE_SECOND.times(3).asMillis(); final Timer timer = new Timer(play ? PlayModes.Live : PlayModes.Replay); AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); @@ -187,14 +181,12 @@ public class EmbeddedMapAndWindChartEntryPoint extends AbstractSailingEntryPoint } mapSettings.setWindUp(windUp); raceMap.updateSettings(mapSettings); - raceMap.onRaceSelectionChange(raceList); final WindChart windChart; if (showWindChart) { - windChart = new WindChart(sailingService, raceSelectionModel, timer, + windChart = new WindChart(sailingService, selectedRaceIdentifier, timer, timeRangeWithZoomProvider, new WindChartSettings(), getStringMessages(), asyncActionsExecutor, /* errorReporter */ EmbeddedMapAndWindChartEntryPoint.this, /* compactChart */ true); - windChart.onRaceSelectionChange(raceList); } else { windChart = null; } 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 219d66c3acb..fde2c93a7cc 100755 --- 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 @@ -2,7 +2,6 @@ package com.sap.sailing.gwt.ui.raceboard; import java.util.Collections; -import java.util.List; import java.util.UUID; import com.google.gwt.core.client.GWT; @@ -17,12 +16,10 @@ import com.google.gwt.user.client.ui.DockLayoutPanel; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.RootLayoutPanel; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; 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.RaceSelectionModel; import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; import com.sap.sailing.gwt.ui.shared.RaceWithCompetitorsDTO; @@ -140,14 +137,12 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { } selectedRace = raceboardData.getRace(); Window.setTitle(selectedRace.getName()); - RaceSelectionModel raceSelectionModel = new RaceSelectionModel(); - List singletonList = Collections.singletonList(selectedRace.getRaceIdentifier()); - raceSelectionModel.setSelection(singletonList); Timer timer = new Timer(PlayModes.Replay, 1000l); AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); - RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, asyncActionsExecutor, this, singletonList, 5000l /* requestInterval*/); + RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingService, asyncActionsExecutor, this, + Collections.singletonList(selectedRace.getRaceIdentifier()), 5000l /* requestInterval*/); RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, getUserService(), asyncActionsExecutor, - raceboardData.getCompetitorAndTheirBoats(), timer, raceSelectionModel, leaderboardName, leaderboardGroupName, eventId, + raceboardData.getCompetitorAndTheirBoats(), timer, selectedRace.getRaceIdentifier(), leaderboardName, leaderboardGroupName, eventId, raceboardViewConfig, RaceBoardEntryPoint.this, getStringMessages(), userAgent, raceTimesInfoProvider, showMapControls); createRaceBoardInOneScreenMode(raceBoardPanel, raceboardViewConfig); 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 414c16d8f1e..4ec5cadfa03 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 @@ -1,7 +1,6 @@ package com.sap.sailing.gwt.ui.raceboard; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -36,8 +35,6 @@ import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; import com.sap.sailing.gwt.ui.client.GlobalNavigationPanel; import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; import com.sap.sailing.gwt.ui.client.MediaServiceAsync; -import com.sap.sailing.gwt.ui.client.RaceSelectionChangeListener; -import com.sap.sailing.gwt.ui.client.RaceSelectionProvider; import com.sap.sailing.gwt.ui.client.RaceTimePanel; import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -79,7 +76,7 @@ import com.sap.sse.security.ui.client.UserService; * @author Frank Mittag, Axel Uhl (d043530) * */ -public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeListener, LeaderboardUpdateListener, PopupPositionProvider { +public class RaceBoardPanel extends SimplePanel implements LeaderboardUpdateListener, PopupPositionProvider { private final SailingServiceAsync sailingService; private final MediaServiceAsync mediaService; private final UUID eventId; @@ -91,7 +88,6 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi private final List componentViewers; private RaceTimePanel timePanel; private final Timer timer; - private final RaceSelectionProvider raceSelectionProvider; private final UserAgentDetails userAgent; private final CompetitorSelectionProvider competitorSelectionProvider; private final TimeRangeWithZoomModel timeRangeWithZoomModel; @@ -126,7 +122,7 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi */ public RaceBoardPanel(SailingServiceAsync sailingService, MediaServiceAsync mediaService, UserService userService, AsyncActionsExecutor asyncActionsExecutor, Map competitorsAndTheirBoats, - Timer timer, RaceSelectionProvider theRaceSelectionProvider, String leaderboardName, + Timer timer, RegattaAndRaceIdentifier selectedRaceIdentifier, String leaderboardName, String leaderboardGroupName, UUID eventId, RaceBoardViewConfiguration raceboardViewConfiguration, ErrorReporter errorReporter, final StringMessages stringMessages, UserAgentDetails userAgent, RaceTimesInfoProvider raceTimesInfoProvider, boolean showMapControls) { @@ -134,7 +130,6 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi this.mediaService = mediaService; this.stringMessages = stringMessages; this.raceboardViewConfiguration = raceboardViewConfiguration; - this.raceSelectionProvider = theRaceSelectionProvider; this.raceTimesInfoProvider = raceTimesInfoProvider; this.errorReporter = errorReporter; this.userAgent = userAgent; @@ -142,8 +137,7 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi this.eventId = eventId; this.currentRaceHasBeenSelectedOnce = false; this.leaderboardName = leaderboardName; - raceSelectionProvider.addRaceSelectionChangeListener(this); - selectedRaceIdentifier = raceSelectionProvider.getSelectedRaces().iterator().next(); + this.selectedRaceIdentifier = selectedRaceIdentifier; this.setRaceBoardName(selectedRaceIdentifier.getRaceName()); this.asyncActionsExecutor = asyncActionsExecutor; FlowPanel mainPanel = new FlowPanel(); @@ -190,11 +184,9 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi } } timePanel = new RaceTimePanel(timer, timeRangeWithZoomModel, stringMessages, raceTimesInfoProvider, - raceboardViewConfiguration.isCanReplayDuringLiveRaces()); + raceboardViewConfiguration.isCanReplayDuringLiveRaces(), selectedRaceIdentifier); timeRangeWithZoomModel.addTimeZoomChangeListener(timePanel); raceTimesInfoProvider.addRaceTimesInfoProviderListener(timePanel); - raceSelectionProvider.addRaceSelectionChangeListener(timePanel); - timePanel.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); } /** @@ -205,18 +197,15 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi boolean showMapControls, RaceMap raceMap, UserService userService) { // create the default leaderboard and select the right race raceTimesInfoProvider.addRaceTimesInfoProviderListener(raceMap); - raceMap.onRaceSelectionChange(Collections.singletonList(selectedRaceIdentifier)); List> components = new ArrayList>(); - competitorChart = new MultiCompetitorRaceChart(sailingService, asyncActionsExecutor, competitorSelectionProvider, raceSelectionProvider, + competitorChart = new MultiCompetitorRaceChart(sailingService, asyncActionsExecutor, competitorSelectionProvider, selectedRaceIdentifier, timer, timeRangeWithZoomModel, stringMessages, errorReporter, true, true, leaderboardGroupName, leaderboardName); - competitorChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); competitorChart.getEntryWidget().setTitle(stringMessages.competitorCharts()); competitorChart.setVisible(false); components.add(competitorChart); - windChart = new WindChart(sailingService, raceSelectionProvider, timer, timeRangeWithZoomModel, new WindChartSettings(), + windChart = new WindChart(sailingService, selectedRaceIdentifier, timer, timeRangeWithZoomModel, new WindChartSettings(), stringMessages, asyncActionsExecutor, errorReporter, /* compactChart */ true); windChart.setVisible(false); - windChart.onRaceSelectionChange(raceSelectionProvider.getSelectedRaces()); windChart.getEntryWidget().setTitle(stringMessages.windChart()); components.add(windChart); editMarkPassingPanel = new EditMarkPassingsPanel(sailingService, selectedRaceIdentifier, @@ -334,10 +323,6 @@ public class RaceBoardPanel extends SimplePanel implements RaceSelectionChangeLi return errorReporter; } - @Override - public void onRaceSelectionChange(List selectedRaces) { - } - public RaceBoardViewConfiguration getConfiguration() { return raceboardViewConfiguration; } From 9d79d00aed98e1891f21a0e3788f4371eb90ad0b Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 2 Dec 2015 11:38:32 +0100 Subject: [PATCH 082/306] use same EntityIdentityComparator for both, single and multi selection model in AbstractTrackedRacesListComposite --- .../AbstractTrackedRacesListComposite.java | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) 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 b4b2e7bb611..945ad3086bf 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 @@ -98,14 +98,11 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl 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(); setWidget(panel); - HorizontalPanel filterPanel = new HorizontalPanel(); panel.add(filterPanel); Label lblFilterRaces = new Label(stringMessages.filterRacesByName() + ":"); @@ -116,23 +113,21 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl noTrackedRacesLabel = new Label(stringMessages.noRacesYet()); noTrackedRacesLabel.setWordWrap(false); panel.add(noTrackedRacesLabel); - AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); raceTable = new CellTable(/* pageSize */10000, tableRes); raceTable.ensureDebugId("TrackedRacesCellTable"); - + final EntityIdentityComparator entityIdentityComparator = new EntityIdentityComparator() { + @Override + public boolean representSameEntity(RaceDTO dto1, RaceDTO dto2) { + return dto1.getRaceIdentifier().equals(dto2.getRaceIdentifier()); + } + }; if (multiSelection) { this.selectionCheckboxColumn = new SelectionCheckboxColumn( tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), - new EntityIdentityComparator() { - - @Override - public boolean representSameEntity(RaceDTO dto1, RaceDTO dto2) { - return dto1.getRaceIdentifier().equals(dto2.getRaceIdentifier()); - } - }) { + entityIdentityComparator) { @Override protected ListDataProvider getListDataProvider() { return raceList; @@ -146,7 +141,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl refreshableSelectionModel = selectionCheckboxColumn.getSelectionModel(); raceTable.setSelectionModel(refreshableSelectionModel, this.selectionCheckboxColumn.getSelectionManager()); } else { - refreshableSelectionModel = new RefreshableSingleSelectionModel(); + refreshableSelectionModel = new RefreshableSingleSelectionModel(entityIdentityComparator); raceTable.setSelectionModel(refreshableSelectionModel); } From c62fb6d3feaec2a81b920cd5ab76138f67465b43 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 2 Dec 2015 13:03:42 +0100 Subject: [PATCH 083/306] Moved/added What new content for mobile RaceBoard --- .../home/solutions/SailingAnalyticsNotes.html | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html index 5bbd603fe5a..473b4b367f2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html @@ -4,6 +4,20 @@

    What's New - SAP Sailing Analytics

    +
    December 2015
    + +
      +
    • The race viewer (RaceBoard.html) now intelligently chooses a layout that suits the screen size. + When only little horizontal space is available, only the sail number and nationality but not the + competitor's long name is displayed by default. When vertical space is scarce, competitor and wind + charts are disabled and more horizontal space can be given to the time slider at the bottom. The + leaderboard section can then be scaled to smaller widths and will reduce the column widths better + than in the past. +
    • +
    • With the changes described above, the RaceBoard is now also accessible from the races page on mobile devices. +
    • +
    +
    November 2015
      @@ -17,13 +31,6 @@
    • Regattas can now be filtered by boat category in the smartphone page. The filter will be shown for events with multiple regattas if there is more than on boat category associated with the event.
    • -
    • The race viewer (RaceBoard.html) now intelligently chooses a layout that suits the screen size. - When only little horizontal space is available, only the sail number and nationality but not the - competitor's long name is displayed by default. When vertical space is scarce, competitor and wind - charts are disabled and more horizontal space can be given to the time slider at the bottom. The - leaderboard section can then be scaled to smaller widths and will reduce the column widths better - than in the past. -
    October 2015
    From b1f502b4cfdcaa8a1176283c55121f2b7d01f276 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 2 Dec 2015 13:14:38 +0100 Subject: [PATCH 084/306] added TODOs for Lukas and some minor whitespace changes --- .../gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java | 6 +++--- .../gwt/ui/adminconsole/TrackedRacesListComposite.java | 4 ++-- .../sap/sailing/gwt/ui/client/CompetitorSelectionModel.java | 6 +++--- .../client/celltable/RefreshableMultiSelectionModel.java | 2 ++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index d5207dd8ae9..f071f525547 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -334,7 +334,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement } replaceLeaderboardInList(availableLeaderboardList, leaderboardName, leaderboard); filterLeaderboardPanel.updateAll(availableLeaderboardList); // also updates leaderboardList provider - refreshableLeaderboardSelectionModel.setSelected(leaderboard, true); + refreshableLeaderboardSelectionModel.setSelected(leaderboard, true); // TODO Lukas: should/will this replace a previously selected element that is compared equal by the EntityIdentityComparator? if (nameOfRaceColumnToSelect != null) { selectRaceColumn(nameOfRaceColumnToSelect); } @@ -354,12 +354,12 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement int index = -1; for (StrippedLeaderboardDTO existingLeaderboard : leaderboardList) { index++; - if (existingLeaderboard.name.equals(leaderboardToReplace)) { + if (existingLeaderboard.name.equals(leaderboardToReplace)) { // TODO Lukas: this should be the EntityIdentityComparator used for the RefreshableSelectionModel break; } } if (index >= 0) { - leaderboardList.set(index, newLeaderboard); + leaderboardList.set(index, newLeaderboard); // TODO Lukas: update refreshable selection model accordingly } } 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 ea5a026932e..54d801f5eeb 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 @@ -154,7 +154,7 @@ public class TrackedRacesListComposite extends AbstractTrackedRacesListComposite @Override protected void makeControlsReactToSelectionChange(Set selectedRaces) { - if(actionButtonsEnabled) { + if (actionButtonsEnabled) { if (selectedRaces.isEmpty()) { btnRemoveRace.setEnabled(false); btnUntrack.setEnabled(false); @@ -164,7 +164,7 @@ public class TrackedRacesListComposite extends AbstractTrackedRacesListComposite btnUntrack.setEnabled(true); btnExport.setEnabled(true); } - } + } } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/CompetitorSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/CompetitorSelectionModel.java index 62793384452..b9627a4acab 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/CompetitorSelectionModel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/CompetitorSelectionModel.java @@ -217,10 +217,10 @@ public class CompetitorSelectionModel implements CompetitorSelectionProvider { Set oldCompetitorsToRemove = new HashSet(allCompetitors); for (CompetitorDTO newCompetitor : newCompetitors) { - if (allCompetitors.contains(newCompetitor)) { - oldCompetitorsToRemove.remove(newCompetitor); + if (allCompetitors.contains(newCompetitor)) { // TODO Lukas: should this use an EntityIdentityComparator instead? + oldCompetitorsToRemove.remove(newCompetitor); // TODO Lukas: the old competitor object remains in allCompetitors; it is equal but doesn't have to be identical to newCompetitor; should it be replaced? } else { - add(newCompetitor, false); + add(newCompetitor, false); // due to the equals definition of CompetitorDTOImpl that compares most fields, state changes lead to a replacement here changed = true; } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 1da25bb83e7..bf18a1dd069 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -19,6 +19,8 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im comp = null; } + // TODO / FIXME: Need to redefine setSelected here: if an element is set selected for which another element is in the current selection that is compared equal by the EntityIdentityComparator then it should be replaced + /** * TODO Lukas: Add Javadoc */ From f5ecc4233159b94f14f1e5decf7cb6026d683364 Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Wed, 2 Dec 2015 13:40:54 +0100 Subject: [PATCH 085/306] added dispatch classes to dashboard bundle --- .../META-INF/MANIFEST.MF | 4 +- .../SailingDashboardsGWT sdm.launch | 2 +- .../SailingDashboardsGWT.launch | 2 +- .../WEB-INF/web.xml | 10 ++ .../gwt/client/DashboardClientFactory.java | 11 ++ .../client/DashboardClientFactoryImpl.java | 19 +++ .../client/RemoteServiceMappingConstants.java | 1 + .../gwt/client/RibDashboardEntryPoint.java | 7 +- .../gwt/client/RibDashboardPanel.java | 55 ++++--- .../GetIDFromRaceThatIsLiveAction.java | 50 ++++++ .../DashboardDispatchRPCServletImpl.java | 73 +++++++++ .../dashboards/gwt/shared/RaceIdDTO.java | 24 +++ .../gwt/shared/dispatch/DashboardAction.java | 17 ++ .../dispatch/DashboardDispatchContext.java | 27 ++++ .../dispatch/DashboardDispatchSystem.java | 11 ++ .../impl/DashboardDispatchContextImpl.java | 62 +++++++ .../impl/DashboardDispatchSystemImpl.java | 13 ++ .../META-INF/MANIFEST.MF | 9 ++ ...WT Remote Profiling (Run-Mode only).launch | 120 +++++++------- .../SailingGWT sdm.launch | 2 +- java/com.sap.sailing.gwt.ui/SailingGWT.launch | 152 +++++++++--------- .../client/impl/DispatchSystemImpl.java | 11 +- .../client/rpcimpl/SimpleDispatch.java | 6 +- .../SailingDispatchSystemImpl.java | 5 + .../SailingServer (No Proxy).launch | 76 ++++----- .../SSETheme_GWT sdm.launch | 2 +- .../com.sap.sse.gwt.theme/SSETheme_GWT.launch | 2 +- .../Security UI sdm.launch | 2 +- 28 files changed, 560 insertions(+), 215 deletions(-) create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatIsLiveAction.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/DashboardDispatchRPCServletImpl.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/RaceIdDTO.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardAction.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchContext.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchSystem.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchContextImpl.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchSystemImpl.java diff --git a/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF b/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF index 15b3a813597..3f43c949b17 100644 --- a/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF @@ -25,7 +25,9 @@ Require-Bundle: com.sap.sailing.domain, com.sap.sse.gwt.adminconsole, org.apache.commons.math;bundle-version="2.1.0" Bundle-ActivationPolicy: lazy -Import-Package: org.osgi.framework;version="1.5.0", +Import-Package: com.sap.sailing.news, + com.sap.sse.util, + org.osgi.framework;version="1.5.0", org.osgi.util.tracker Web-ContextPath: /dashboards Bundle-ClassPath: WEB-INF/classes/, diff --git a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch index 3df14c0b671..aefd1e3ffb6 100755 --- a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch +++ b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT sdm.launch @@ -43,5 +43,5 @@ - + diff --git a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch index 4d2ae11f9e2..463a6cf721f 100755 --- a/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch +++ b/java/com.sap.sailing.dashboards.gwt/SailingDashboardsGWT.launch @@ -43,5 +43,5 @@ - + diff --git a/java/com.sap.sailing.dashboards.gwt/WEB-INF/web.xml b/java/com.sap.sailing.dashboards.gwt/WEB-INF/web.xml index 46e9142f142..2181d54550d 100644 --- a/java/com.sap.sailing.dashboards.gwt/WEB-INF/web.xml +++ b/java/com.sap.sailing.dashboards.gwt/WEB-INF/web.xml @@ -39,6 +39,16 @@ SailingService /service/sailing + + + Dispatch Service + DispatchService + com.sap.sailing.dashboards.gwt.server.DashboardDispatchRPCServletImpl + + + DispatchService + /service/dispatch + diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java new file mode 100644 index 00000000000..f516121acc0 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java @@ -0,0 +1,11 @@ +package com.sap.sailing.dashboards.gwt.client; + +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchSystem; + +/** + * @author Alexander Ries (D062114) + * + */ +public interface DashboardClientFactory{ + DashboardDispatchSystem getDispatch(); +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java new file mode 100644 index 00000000000..b704185a2fb --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java @@ -0,0 +1,19 @@ +package com.sap.sailing.dashboards.gwt.client; + +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchSystem; +import com.sap.sailing.dashboards.gwt.shared.dispatch.impl.DashboardDispatchSystemImpl; + +/** + * @author Alexander Ries (D062114) + * + */ +public class DashboardClientFactoryImpl implements DashboardClientFactory{ + + private final DashboardDispatchSystem dispatch = new DashboardDispatchSystemImpl(); + + @Override + public DashboardDispatchSystem getDispatch() { + return dispatch; + } + +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RemoteServiceMappingConstants.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RemoteServiceMappingConstants.java index 3e3f281bbc6..f008fede3b2 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RemoteServiceMappingConstants.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RemoteServiceMappingConstants.java @@ -18,4 +18,5 @@ public interface RemoteServiceMappingConstants { public static final String sailingServiceRemotePath = "service/sailing"; public static final String ribdashboardServiceRemotePath = "service/ribdashboard"; + public static final String ribdashboardDispatchServiceRemotePath = "service/dispatch"; } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java index 8b143644190..4cb66669148 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java @@ -39,14 +39,15 @@ public class RibDashboardEntryPoint extends AbstractEntryPoint { RibDashboardDataRetriever dataRetriever = new RibDashboardDataRetriever(ribdashboardService); applyMGWTSettings(); - RibDashboardPanel ribDashboardPanel = new RibDashboardPanel(ribdashboardService, sailingService, dataRetriever); + DashboardClientFactory dashboardClientFactory = new DashboardClientFactoryImpl(); + RibDashboardPanel ribDashboardPanel = new RibDashboardPanel(dashboardClientFactory); RootLayoutPanel.get().add(ribDashboardPanel); - WindBotDataRetriever windBotDataRetriever = new WindBotDataRetriever(ribdashboardService, sailingService); + /*WindBotDataRetriever windBotDataRetriever = new WindBotDataRetriever(ribdashboardService, sailingService); windBotDataRetriever.addNumberOfWindBotsChangeListeners(ribDashboardPanel); Timer dashboardTimer = new Timer(PlayModes.Live); dashboardTimer.setRefreshInterval(DASHBOARD_RERFRESH_INTERVAL); dashboardTimer.addTimeListener(windBotDataRetriever); - dashboardTimer.addTimeListener(dataRetriever); + dashboardTimer.addTimeListener(dataRetriever);*/ } private void applyMGWTSettings() { diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java index aefb8c550eb..89eb096aa77 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java @@ -18,6 +18,7 @@ import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.LayoutPanel; import com.google.gwt.user.client.ui.Widget; import com.googlecode.mgwt.ui.client.widget.carousel.Carousel; +import com.sap.sailing.dashboards.gwt.client.actions.GetIDFromRaceThatIsLiveAction; import com.sap.sailing.dashboards.gwt.client.dataretriever.NumberOfWindBotsChangeListener; import com.sap.sailing.dashboards.gwt.client.dataretriever.RibDashboardDataRetriever; import com.sap.sailing.dashboards.gwt.client.dataretriever.WindBotDataRetrieverProvider; @@ -28,6 +29,7 @@ import com.sap.sailing.dashboards.gwt.client.startlineadvantage.StartLineAdvanta import com.sap.sailing.dashboards.gwt.client.startlineadvantage.StartlineAdvantagesByWindComponent; import com.sap.sailing.dashboards.gwt.client.windchart.WindBotComponent; import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; +import com.sap.sailing.dashboards.gwt.shared.RaceIdDTO; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -91,22 +93,24 @@ public class RibDashboardPanel extends Composite implements NumberOfWindBotsChan private List windBotComponents; private StringMessages stringConstants; private RibDashboardServiceAsync ribDashboardService; + DashboardClientFactory dashboardClientFactory; private static final Logger logger = Logger.getLogger(RibDashboardPanel.class.getName()); - public RibDashboardPanel(RibDashboardServiceAsync ribDashboardService, SailingServiceAsync sailingServiceAsync, RibDashboardDataRetriever ribDashboardDataRetriever) { - this.ribDashboardService = ribDashboardService; - windBotComponents = new ArrayList(); - startanalysisComponent = new StartlineAnalysisComponent(ribDashboardService, sailingServiceAsync); - startlineAdvantagesByWindComponent = new StartlineAdvantagesByWindComponent(ribDashboardService); - startlineAdvantageByGeometryComponent = new StartLineAdvantageByGeometryComponent(ribDashboardDataRetriever); + public RibDashboardPanel(DashboardClientFactory dashboardClientFactory) { + this.dashboardClientFactory = dashboardClientFactory; +// this.ribDashboardService = ribDashboardService; +// windBotComponents = new ArrayList(); +// startanalysisComponent = new StartlineAnalysisComponent(ribDashboardService, sailingServiceAsync); +// startlineAdvantagesByWindComponent = new StartlineAdvantagesByWindComponent(ribDashboardService); +// startlineAdvantageByGeometryComponent = new StartLineAdvantageByGeometryComponent(ribDashboardDataRetriever); stringConstants = StringMessages.INSTANCE; initWidget(uiBinder.createAndBindUi(this)); - initLogos(); - windcharthint.getElement().setInnerText(stringConstants.dashboardWindChartHint()); - windloadinghintleft.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
    "+stringConstants.dashboardWindBotLoadingMessage()); - windloadinghintright.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
    "+stringConstants.dashboardWindBotLoadingMessage()); +// initLogos(); +// windcharthint.getElement().setInnerText(stringConstants.dashboardWindChartHint()); +// windloadinghintleft.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
    "+stringConstants.dashboardWindBotLoadingMessage()); +// windloadinghintright.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
    "+stringConstants.dashboardWindBotLoadingMessage()); startUpdatingRaceLabelIn20SecondInterval(); - loadAndAddEventLogo(sailingServiceAsync); +// loadAndAddEventLogo(sailingServiceAsync); initAndAddWrongOrientationNotification(); } @@ -176,26 +180,29 @@ public class RibDashboardPanel extends Composite implements NumberOfWindBotsChan private void startUpdatingRaceLabelIn20SecondInterval() { Timer timer = new Timer(PlayModes.Live); - timer.setRefreshInterval(20000); - timer.addTimeListener( new TimeListener() { - + timer.setRefreshInterval(5000); + timer.addTimeListener(new TimeListener() { + @Override public void timeChanged(Date newTime, Date oldTime) { String leaderboardNameParameterValue = DashboardURLParameters.LEADERBOARD_NAME.getValue(); if (leaderboardNameParameterValue != null) { - ribDashboardService.getIDFromRaceThatIsLive(leaderboardNameParameterValue, - new AsyncCallback() { - @Override - public void onSuccess(RegattaAndRaceIdentifier result) { - if (result != null) { - logger.log(Level.INFO, "Updating UI with RibDashboardRaceInfoDTO race name"); - setHeaderText(result.getRaceName()); - } - } + dashboardClientFactory.getDispatch().execute( + new GetIDFromRaceThatIsLiveAction(leaderboardNameParameterValue), + new AsyncCallback() { @Override public void onFailure(Throwable caught) { - logger.log(Level.INFO, "Failed to received RegattaAndRaceIdentifier from race that is live, " + caught.getMessage()); + logger.log(Level.INFO, + "Failed to received RegattaAndRaceIdentifier from race that is live, "+caught.getMessage()); + } + + @Override + public void onSuccess(RaceIdDTO result) { + if (result != null) { + logger.log(Level.INFO, "Updating UI with RibDashboardRaceInfoDTO race name"); + setHeaderText(result.getRaceId().getRaceName()); + } } }); } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatIsLiveAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatIsLiveAction.java new file mode 100644 index 00000000000..cd5a3952d54 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatIsLiveAction.java @@ -0,0 +1,50 @@ +package com.sap.sailing.dashboards.gwt.client.actions; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.dashboards.gwt.shared.RaceIdDTO; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardAction; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +/** + * @author Alexander Ries (D062114) + * + */ +public class GetIDFromRaceThatIsLiveAction implements DashboardAction { + + private String leaderboarName; + + @SuppressWarnings("unused") + private GetIDFromRaceThatIsLiveAction() { + } + + public GetIDFromRaceThatIsLiveAction(String leaderboardName) { + this.leaderboarName = leaderboardName; + } + + @Override + @GwtIncompatible + public RaceIdDTO execute(DashboardDispatchContext ctx) throws DispatchException { + RaceIdDTO result = new RaceIdDTO(); + TrackedRace liveRace = null; + Leaderboard leaderboard = ctx.getRacingEventService().getLeaderboardByName(this.leaderboarName); + if (leaderboard != null) { + for (RaceColumn column : leaderboard.getRaceColumns()) { + for (Fleet fleet : column.getFleets()) { + TrackedRace race = column.getTrackedRace(fleet); + if (race != null && race.isLive(MillisecondsTimePoint.now())) { + liveRace = race; + } + } + } + } + if (liveRace != null) + result.setRaceId(liveRace.getRaceIdentifier()); + return result; + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/DashboardDispatchRPCServletImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/DashboardDispatchRPCServletImpl.java new file mode 100644 index 00000000000..8e9ea5781e5 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/DashboardDispatchRPCServletImpl.java @@ -0,0 +1,73 @@ +package com.sap.sailing.dashboards.gwt.server; + +import java.util.UUID; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.osgi.framework.BundleContext; +import org.osgi.util.tracker.ServiceTracker; + +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; +import com.sap.sailing.dashboards.gwt.shared.dispatch.impl.DashboardDispatchContextImpl; +import com.sap.sailing.gwt.dispatch.client.Action; +import com.sap.sailing.gwt.dispatch.client.Result; +import com.sap.sailing.gwt.dispatch.client.ResultWrapper; +import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; +import com.sap.sailing.gwt.dispatch.client.exceptions.ServerDispatchException; +import com.sap.sailing.gwt.dispatch.client.rpcimpl.DispatchRPC; +import com.sap.sailing.gwt.dispatch.client.rpcimpl.RequestWrapper; +import com.sap.sailing.gwt.ui.server.Activator; +import com.sap.sailing.gwt.ui.server.ProxiedRemoteServiceServlet; +import com.sap.sailing.server.RacingEventService; +import com.sap.sse.util.ServiceTrackerFactory; + +public class DashboardDispatchRPCServletImpl extends ProxiedRemoteServiceServlet implements DispatchRPC { + private static final long serialVersionUID = -245230476512348999L; + private static final Logger logger = Logger.getLogger(DashboardDispatchRPCServletImpl.class.getName()); + + private final ServiceTracker racingEventServiceTracker; + + public DashboardDispatchRPCServletImpl() { + final BundleContext context = Activator.getDefault(); + + racingEventServiceTracker = ServiceTrackerFactory.createAndOpen(context, RacingEventService.class); + } + + @Override + public > ResultWrapper execute( + RequestWrapper request) throws DispatchException { + A action = request.getAction(); + long start = System.currentTimeMillis(); + try { + R executionResult = action.execute( + new DashboardDispatchContextImpl(request.getCurrentClientTime(), + racingEventServiceTracker.getService(), request + .getClientLocaleName(), getThreadLocalRequest())); + return new ResultWrapper(executionResult); + } catch (DispatchException d) { + logger.log(Level.WARNING, "Server exception", d); + throw d; + } catch (Throwable t) { + String serverExceptionUUID = UUID.randomUUID().toString(); + logger.log(Level.SEVERE, "Uncaught server exception id: " + serverExceptionUUID, t); + throw new ServerDispatchException(serverExceptionUUID, t); + } finally { + long duration = System.currentTimeMillis() - start; + final Level logLevel; + if (duration < 100) { + logLevel = Level.FINEST; + } else if (duration < 500) { + logLevel = Level.INFO; + } else { + logLevel = Level.WARNING; + } + logger.log(logLevel, "Dispatch took " + duration + "ms for " + action.getClass().getSimpleName()); + } + } + + @Override + protected void doUnexpectedFailure(Throwable e) { + logger.log(Level.WARNING, "GWT RPC Exception: " + e.getMessage(), e); + super.doUnexpectedFailure(e); + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/RaceIdDTO.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/RaceIdDTO.java new file mode 100644 index 00000000000..5d473cb8948 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/RaceIdDTO.java @@ -0,0 +1,24 @@ +package com.sap.sailing.dashboards.gwt.shared; + +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.gwt.dispatch.client.Result; + +/** + * @author Alexander Ries (D062114) + * + */ +public class RaceIdDTO implements Result { + + private RegattaAndRaceIdentifier raceId; + + public RaceIdDTO() { + } + + public RegattaAndRaceIdentifier getRaceId() { + return raceId; + } + + public void setRaceId(RegattaAndRaceIdentifier raceId) { + this.raceId = raceId; + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardAction.java new file mode 100644 index 00000000000..abf992cab72 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardAction.java @@ -0,0 +1,17 @@ +package com.sap.sailing.dashboards.gwt.shared.dispatch; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.gwt.dispatch.client.Action; +import com.sap.sailing.gwt.dispatch.client.Result; +import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; + +/** + * @author Alexander Ries (D062114) + * + */ +public interface DashboardAction extends Action { + + @GwtIncompatible + R execute(DashboardDispatchContext ctx) throws DispatchException; + +} \ No newline at end of file diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchContext.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchContext.java new file mode 100644 index 00000000000..c54e21f3957 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchContext.java @@ -0,0 +1,27 @@ +package com.sap.sailing.dashboards.gwt.shared.dispatch; + +import java.util.Date; +import java.util.Locale; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.gwt.dispatch.client.DispatchContext; +import com.sap.sailing.server.RacingEventService; + +/** + * @author Alexander Ries (D062114) + * + */ +public interface DashboardDispatchContext extends DispatchContext { + + @GwtIncompatible + RacingEventService getRacingEventService(); + + @GwtIncompatible + String getClientLocaleName(); + + @GwtIncompatible + Locale getClientLocale(); + + @GwtIncompatible + Date getCurrentClientTime(); +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchSystem.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchSystem.java new file mode 100644 index 00000000000..4902a45e242 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchSystem.java @@ -0,0 +1,11 @@ +package com.sap.sailing.dashboards.gwt.shared.dispatch; + +import com.sap.sailing.gwt.dispatch.client.impl.DispatchSystem; + +/** + * @author Alexander Ries (D062114) + * + */ +public interface DashboardDispatchSystem extends DispatchSystem{ + +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchContextImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchContextImpl.java new file mode 100644 index 00000000000..b625294cecb --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchContextImpl.java @@ -0,0 +1,62 @@ +package com.sap.sailing.dashboards.gwt.shared.dispatch.impl; + +import java.net.URL; +import java.util.Date; +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; + +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; +import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; +import com.sap.sailing.gwt.server.HomeServiceUtil; +import com.sap.sailing.server.RacingEventService; + +/** + * @author Alexander Ries (D062114) + * + */ +public class DashboardDispatchContextImpl implements DashboardDispatchContext{ + private final RacingEventService racingEventService; + private final Date currentClientTime; +// private final Date currentServerTime = new Date(); + private String clientLocaleName; + private final HttpServletRequest request; + + public DashboardDispatchContextImpl(Date currentClientTime, RacingEventService racingEventService, String clientLocaleName, HttpServletRequest request) { + this.currentClientTime = currentClientTime; + this.racingEventService = racingEventService; + this.clientLocaleName = clientLocaleName; + this.request = request; + } + + @Override + public RacingEventService getRacingEventService() { + return racingEventService; + } + + @Override + public Date getCurrentClientTime() { + return currentClientTime; + } + + @Override + public String getClientLocaleName() { + return clientLocaleName; + } + + @Override + public Locale getClientLocale() { + return Locale.forLanguageTag(clientLocaleName); + } + + @Override + public HttpServletRequest getRequest() { + return request; + } + + @Override + public URL getRequestBaseURL() throws DispatchException { + return HomeServiceUtil.getRequestBaseURL(request); + } + +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchSystemImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchSystemImpl.java new file mode 100644 index 00000000000..0e9a9986656 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchSystemImpl.java @@ -0,0 +1,13 @@ +package com.sap.sailing.dashboards.gwt.shared.dispatch.impl; + +import com.sap.sailing.dashboards.gwt.client.RemoteServiceMappingConstants; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchSystem; +import com.sap.sailing.gwt.dispatch.client.impl.DispatchSystemImpl; + +public class DashboardDispatchSystemImpl extends DispatchSystemImpl implements DashboardDispatchSystem { + + public DashboardDispatchSystemImpl() { + super(RemoteServiceMappingConstants.ribdashboardDispatchServiceRemotePath); + } +} diff --git a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF index 91e8a38b32f..43aac8438a7 100644 --- a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF @@ -71,6 +71,15 @@ TrackLifecycle: true Export-Package: com.google.gwt.user.client.rpc.core.com.sap.sailing.domain.common.confidence.impl, com.google.gwt.user.client.rpc.core.com.sap.sailing.domain.common.impl, com.google.gwt.user.client.rpc.core.com.sap.sailing.domain.common.tracking.impl, + com.sap.sailing.gwt.dispatch.client, + com.sap.sailing.gwt.dispatch.client.batching, + com.sap.sailing.gwt.dispatch.client.caching, + com.sap.sailing.gwt.dispatch.client.exceptions, + com.sap.sailing.gwt.dispatch.client.impl, + com.sap.sailing.gwt.dispatch.client.rpcimpl, + com.sap.sailing.gwt.home.communication, + com.sap.sailing.gwt.home.server, + com.sap.sailing.gwt.server, com.sap.sailing.gwt.ui.actions, com.sap.sailing.gwt.ui.client, com.sap.sailing.gwt.ui.client.shared.charts, diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT Remote Profiling (Run-Mode only).launch b/java/com.sap.sailing.gwt.ui/SailingGWT Remote Profiling (Run-Mode only).launch index 3ffcab00446..4e32ff66b23 100755 --- a/java/com.sap.sailing.gwt.ui/SailingGWT Remote Profiling (Run-Mode only).launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT Remote Profiling (Run-Mode only).launch @@ -1,60 +1,60 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch index 762b510c1e3..2b63f334eb7 100644 --- a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch @@ -75,5 +75,5 @@ - + diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT.launch b/java/com.sap.sailing.gwt.ui/SailingGWT.launch index 4f94e217a34..74a6774a504 100755 --- a/java/com.sap.sailing.gwt.ui/SailingGWT.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT.launch @@ -1,76 +1,76 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/impl/DispatchSystemImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/impl/DispatchSystemImpl.java index 5bb331f988f..a34252f80fc 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/impl/DispatchSystemImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/impl/DispatchSystemImpl.java @@ -15,10 +15,15 @@ import com.sap.sailing.gwt.dispatch.client.rpcimpl.SimpleDispatch; public class DispatchSystemImpl implements DispatchSystem { - private final SimpleDispatch simpleDispatch = new SimpleDispatch(null); - private final DispatchAsync dispatch = new CachingDispatch(new AutomaticBatchingDispatch( - simpleDispatch)); + private final SimpleDispatch simpleDispatch; + private final DispatchAsync dispatch; + public DispatchSystemImpl(String dispatchRPCPath) { + simpleDispatch = new SimpleDispatch(dispatchRPCPath); + dispatch = new CachingDispatch(new AutomaticBatchingDispatch( + simpleDispatch)); + } + @Override public > void execute(final A action, final AsyncCallback callback) { // TODO: client side execution time logging diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/rpcimpl/SimpleDispatch.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/rpcimpl/SimpleDispatch.java index 63b564c4e04..49ab4df3730 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/rpcimpl/SimpleDispatch.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/rpcimpl/SimpleDispatch.java @@ -9,8 +9,6 @@ import com.sap.sailing.gwt.dispatch.client.DispatchAsync; import com.sap.sailing.gwt.dispatch.client.DispatchContext; import com.sap.sailing.gwt.dispatch.client.Result; import com.sap.sailing.gwt.dispatch.client.ResultWrapper; -import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; -import com.sap.sailing.gwt.ui.client.SailingClientFactory; public class SimpleDispatch implements DispatchAsync { @@ -18,8 +16,8 @@ public class SimpleDispatch implements DispatchAsyn private long clientServerOffset = 0; - public SimpleDispatch(SailingClientFactory clientFactory) { - ((ServiceDefTarget) dispatchRPC).setServiceEntryPoint(RemoteServiceMappingConstants.dispatchServiceRemotePath); + public SimpleDispatch(String dispatchRPCPath) { + ((ServiceDefTarget) dispatchRPC).setServiceEntryPoint(dispatchRPCPath); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchSystemImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchSystemImpl.java index f6a83a87e13..db62ef9885d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchSystemImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/SailingDispatchSystemImpl.java @@ -1,7 +1,12 @@ package com.sap.sailing.gwt.home.communication; import com.sap.sailing.gwt.dispatch.client.impl.DispatchSystemImpl; +import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; public class SailingDispatchSystemImpl extends DispatchSystemImpl implements SailingDispatchSystem { + + public SailingDispatchSystemImpl() { + super(RemoteServiceMappingConstants.dispatchServiceRemotePath); + } } diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy).launch b/java/com.sap.sailing.server/SailingServer (No Proxy).launch index 1778a15302f..ad2ef4043be 100644 --- a/java/com.sap.sailing.server/SailingServer (No Proxy).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy).launch @@ -1,38 +1,38 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sse.gwt.theme/SSETheme_GWT sdm.launch b/java/com.sap.sse.gwt.theme/SSETheme_GWT sdm.launch index e61889df28b..2523720b3c5 100644 --- a/java/com.sap.sse.gwt.theme/SSETheme_GWT sdm.launch +++ b/java/com.sap.sse.gwt.theme/SSETheme_GWT sdm.launch @@ -23,5 +23,5 @@ - + diff --git a/java/com.sap.sse.gwt.theme/SSETheme_GWT.launch b/java/com.sap.sse.gwt.theme/SSETheme_GWT.launch index 497ba323966..b592dd92581 100644 --- a/java/com.sap.sse.gwt.theme/SSETheme_GWT.launch +++ b/java/com.sap.sse.gwt.theme/SSETheme_GWT.launch @@ -22,5 +22,5 @@ - + diff --git a/java/com.sap.sse.security.ui/Security UI sdm.launch b/java/com.sap.sse.security.ui/Security UI sdm.launch index 7ff5e9979c7..e15e8f7ab9e 100755 --- a/java/com.sap.sse.security.ui/Security UI sdm.launch +++ b/java/com.sap.sse.security.ui/Security UI sdm.launch @@ -44,5 +44,5 @@ - + From e5b4858ff8897adfb89a0a3b1d7a26b7a2d44f15 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 2 Dec 2015 14:55:46 +0100 Subject: [PATCH 086/306] newSelectionModel: added method setSelected to the RefreshableSelectionModels --- .../RefreshableMultiSelectionModel.java | 28 +++++++++++++-- .../RefreshableSingleSelectionModel.java | 35 +++++++++++++++++-- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index bf18a1dd069..6d41fdd3af2 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -19,8 +19,6 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im comp = null; } - // TODO / FIXME: Need to redefine setSelected here: if an element is set selected for which another element is in the current selection that is compared equal by the EntityIdentityComparator then it should be replaced - /** * TODO Lukas: Add Javadoc */ @@ -42,6 +40,32 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im return comp; } + /* + * TODO / FIXME: Need to redefine setSelected here: if an element is set selected for which another element is in + * the current selection that is compared equal by the EntityIdentityComparator then it should be replaced + */ + @Override + public void setSelected(T item, boolean selected) { + if (comp == null) { + super.setSelected(item, selected); + } else { + T wasSelectedBefore = null; + Set selectedSet = getSelectedSet(); + for (T it : selectedSet) { + if(comp.representSameEntity(it, item)) { + wasSelectedBefore = it; + break; + } + } + if(wasSelectedBefore == null) { + super.setSelected(wasSelectedBefore, false); //This old version of item will be deleted with the next clear() + super.setSelected(item, selected); + } else { + super.setSelected(item, selected); + } + } + } + @Override public void refreshSelectionModel(Iterable newObjects) { final Set selectedSet = getSelectedSet(); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index bf198378979..f513407da1a 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -1,9 +1,14 @@ package com.sap.sse.gwt.client.celltable; +import java.util.Set; import com.google.gwt.view.client.ProvidesKey; import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SingleSelectionModel; +/** + * TODO Lukas: Add Javadoc + */ + public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel { private final EntityIdentityComparator comp; @@ -11,12 +16,16 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel super(); comp = null; } - + /** + * TODO Lukas: Add Javadoc + */ public RefreshableSingleSelectionModel(EntityIdentityComparator comp) { super(); this.comp=comp; } - + /** + * TODO Lukas: Add Javadoc + */ public RefreshableSingleSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { super(keyProvider); this.comp =comp; @@ -26,6 +35,28 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel public EntityIdentityComparator getEntityIdentityComparator() { return comp; } + + @Override + public void setSelected(T item, boolean selected) { + if (comp == null) { + super.setSelected(item, selected); + } else { + T wasSelectedBefore = null; + Set selectedSet = getSelectedSet(); + for (T it : selectedSet) { + if(comp.representSameEntity(it, item)) { + wasSelectedBefore = it; + break; + } + } + if(wasSelectedBefore == null) { + super.setSelected(wasSelectedBefore, false); //This old version of item will be deleted with the next clear() + super.setSelected(item, selected); + } else { + super.setSelected(item, selected); + } + } + } @Override public void refreshSelectionModel(Iterable newObjects) { From aebcd513c74e2572901d63129503478d84013a35 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 2 Dec 2015 15:44:36 +0100 Subject: [PATCH 087/306] newSelectionModel: removed constructors form SelectionCheckboxColumn.java --- .../ui/adminconsole/EventListComposite.java | 19 ++++++++---- .../ui/adminconsole/EventManagementPanel.java | 27 ++++++----------- .../ui/adminconsole/RegattaListComposite.java | 25 +++++++++++----- .../adminconsole/RegattaManagementPanel.java | 15 ++-------- .../StructureImportListComposite.java | 18 +++++++----- .../StructureImportManagementPanel.java | 12 ++------ .../controls/SelectionCheckboxColumn.java | 29 +------------------ 7 files changed, 57 insertions(+), 88 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 18548f653c6..45c75799d84 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -47,6 +47,7 @@ import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -90,10 +91,8 @@ public class EventListComposite extends Composite implements EventsRefresher, Le private final AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); - public EventListComposite(final SailingServiceAsync sailingService, final RefreshableMultiSelectionModel refreshableEventSelectionModel, - final ErrorReporter errorReporter, final StringMessages stringMessages) { + public EventListComposite(final SailingServiceAsync sailingService, final ErrorReporter errorReporter, final StringMessages stringMessages) { this.sailingService = sailingService; - this.refreshableEventSelectionModel = refreshableEventSelectionModel; this.stringMessages = stringMessages; this.errorReporter = errorReporter; availableLeaderboardGroups = Collections.emptyList(); @@ -184,9 +183,15 @@ public class EventListComposite extends Composite implements EventsRefresher, Le eventListDataProvider.addDataDisplay(table); table.setWidth("100%"); - SelectionCheckboxColumn eventSelectionCheckboxColumn = new SelectionCheckboxColumn(refreshableEventSelectionModel, tableRes.cellTableStyle().cellTableCheckboxSelected(), + SelectionCheckboxColumn eventSelectionCheckboxColumn = new SelectionCheckboxColumn( + tableRes.cellTableStyle().cellTableCheckboxSelected(), tableRes.cellTableStyle().cellTableCheckboxDeselected(), - tableRes.cellTableStyle().cellTableCheckboxColumnCell()) { + tableRes.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { + @Override + public boolean representSameEntity(EventDTO dto1, EventDTO dto2) { + return dto1.id.equals(dto2.id); + } + }) { @Override protected ListDataProvider getListDataProvider() { return eventListDataProvider; @@ -599,4 +604,8 @@ public class EventListComposite extends Composite implements EventsRefresher, Le public List getAllEvents() { return allEvents; } + + public RefreshableMultiSelectionModel getRefreshableMultiSelectionModel() { + return refreshableEventSelectionModel; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventManagementPanel.java index 29a92361c82..24bf4f7ff9a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventManagementPanel.java @@ -13,7 +13,6 @@ import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.EventDTO; import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; /** @@ -39,16 +38,17 @@ public class EventManagementPanel extends SimplePanel implements EventsRefresher VerticalPanel eventsContentPanel = new VerticalPanel(); eventsPanel.setContentWidget(eventsContentPanel); - refreshableEventSelectionModel = new RefreshableMultiSelectionModel<>(new EntityIdentityComparator() { - - @Override - public boolean representSameEntity(EventDTO dto1, EventDTO dto2) { - return dto1.id.equals(dto2.id); - } - }); + eventListComposite = new EventListComposite(sailingService, errorReporter, stringMessages); + eventListComposite.ensureDebugId("EventListComposite"); + eventsContentPanel.add(eventListComposite); + eventDetailsComposite = new EventDetailsComposite(sailingService, errorReporter, stringMessages); + eventDetailsComposite.ensureDebugId("EventDetailsComposite"); + eventDetailsComposite.setVisible(false); + mainPanel.add(eventDetailsComposite); + + refreshableEventSelectionModel = eventListComposite.getRefreshableMultiSelectionModel(); refreshableEventSelectionModel.addSelectionChangeHandler(new Handler() { - @Override public void onSelectionChange(SelectionChangeEvent event) { final Set selectedEvents = refreshableEventSelectionModel.getSelectedSet(); @@ -67,15 +67,6 @@ public class EventManagementPanel extends SimplePanel implements EventsRefresher } } }); - - eventListComposite = new EventListComposite(sailingService, refreshableEventSelectionModel, errorReporter, stringMessages); - eventListComposite.ensureDebugId("EventListComposite"); - eventsContentPanel.add(eventListComposite); - - eventDetailsComposite = new EventDetailsComposite(sailingService, errorReporter, stringMessages); - eventDetailsComposite.ensureDebugId("EventDetailsComposite"); - eventDetailsComposite.setVisible(false); - mainPanel.add(eventDetailsComposite); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index 92261297001..1d3752a6f2f 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -40,6 +40,7 @@ import com.sap.sse.common.Util; 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.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -75,11 +76,10 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer } } - public RegattaListComposite(final SailingServiceAsync sailingService, - final RefreshableMultiSelectionModel refreshableRegattaMultiSelectionModel, RegattaRefresher regattaRefresher, + @SuppressWarnings("unchecked") + public RegattaListComposite(final SailingServiceAsync sailingService, RegattaRefresher regattaRefresher, final ErrorReporter errorReporter, final StringMessages stringMessages) { this.sailingService = sailingService; - this.refreshableRegattaMultiSelectionModel = refreshableRegattaMultiSelectionModel; this.regattaRefresher = regattaRefresher; this.errorReporter = errorReporter; this.stringMessages = stringMessages; @@ -96,8 +96,9 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer panel.add(noRegattasLabel); regattaListDataProvider = new ListDataProvider(); - regattaTable = createRegattaTable(this.refreshableRegattaMultiSelectionModel); + regattaTable = createRegattaTable(); regattaTable.ensureDebugId("RegattasCellTable"); + refreshableRegattaMultiSelectionModel = (RefreshableMultiSelectionModel) regattaTable.getSelectionModel(); regattaTable.setVisible(false); filterablePanelRegattas = new LabeledAbstractFilterablePanel(filterRegattasLabel, allRegattas, @@ -123,14 +124,20 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer return refreshableRegattaMultiSelectionModel.addSelectionChangeHandler(handler); } - protected CellTable createRegattaTable(RefreshableMultiSelectionModel selectionmodel) { + protected CellTable createRegattaTable() { CellTable table = new CellTable(/* pageSize */10000, tableRes); regattaListDataProvider.addDataDisplay(table); table.setWidth("100%"); - SelectionCheckboxColumn regattaSelectionCheckboxColumn = new SelectionCheckboxColumn(selectionmodel, tableRes.cellTableStyle().cellTableCheckboxSelected(), + SelectionCheckboxColumn regattaSelectionCheckboxColumn = new SelectionCheckboxColumn( + tableRes.cellTableStyle().cellTableCheckboxSelected(), tableRes.cellTableStyle().cellTableCheckboxDeselected(), - tableRes.cellTableStyle().cellTableCheckboxColumnCell()) { + tableRes.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { + @Override + public boolean representSameEntity(RegattaDTO dto1, RegattaDTO dto2) { + return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); + } + }) { @Override protected ListDataProvider getListDataProvider() { return regattaListDataProvider; @@ -233,7 +240,6 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer table.addColumn(rankingMetricColumn, stringMessages.rankingMetric()); table.addColumn(regattaActionColumn, stringMessages.actions()); table.setSelectionModel(regattaSelectionCheckboxColumn.getSelectionModel(), regattaSelectionCheckboxColumn.getSelectionManager()); - return table; } @@ -353,4 +359,7 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer return allRegattas; } + public RefreshableMultiSelectionModel getRefreshableMultiSelectionModel() { + return refreshableRegattaMultiSelectionModel; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java index a195b3f00fd..a8d35952281 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaManagementPanel.java @@ -30,7 +30,6 @@ import com.sap.sailing.gwt.ui.shared.RegattaDTO; import com.sap.sailing.gwt.ui.shared.SeriesDTO; import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; @@ -103,15 +102,10 @@ public class RegattaManagementPanel extends SimplePanel implements RegattasDispl }); regattaManagementControlsPanel.add(removeRegattaButton); regattasContentPanel.add(regattaManagementControlsPanel); - - refreshableRegattaMultiSelectionModel = new RefreshableMultiSelectionModel<>(new EntityIdentityComparator() { - @Override - public boolean representSameEntity(RegattaDTO dto1, RegattaDTO dto2) { - return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); - } - }); + regattaListComposite = new RegattaListComposite(sailingService, regattaRefresher, errorReporter, stringMessages); + regattaListComposite.ensureDebugId("RegattaListComposite"); + refreshableRegattaMultiSelectionModel = regattaListComposite.getRefreshableMultiSelectionModel(); refreshableRegattaMultiSelectionModel.addSelectionChangeHandler(new Handler() { - @Override public void onSelectionChange(SelectionChangeEvent event) { List selectedRegattas = new ArrayList<>(refreshableRegattaMultiSelectionModel.getSelectedSet()); @@ -134,9 +128,6 @@ public class RegattaManagementPanel extends SimplePanel implements RegattasDispl removeRegattaButton.setEnabled(!selectedRegattas.isEmpty()); } }); - - regattaListComposite = new RegattaListComposite(sailingService, refreshableRegattaMultiSelectionModel, regattaRefresher, errorReporter, stringMessages); - regattaListComposite.ensureDebugId("RegattaListComposite"); regattasContentPanel.add(regattaListComposite); regattaDetailsComposite = new RegattaDetailsComposite(sailingService, regattaRefresher, errorReporter, stringMessages); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java index 5e1edc6c4e2..afdd99e7825 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java @@ -14,7 +14,7 @@ import com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn; import com.sap.sailing.gwt.ui.shared.RegattaDTO; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; public class StructureImportListComposite extends RegattaListComposite implements RegattasDisplayer { @@ -25,25 +25,29 @@ public class StructureImportListComposite extends RegattaListComposite implement RegattaStructure getRegattaStructure(RegattaDTO regatta); } - public StructureImportListComposite(final SailingServiceAsync sailingService, - final RefreshableMultiSelectionModel refreshableRegattaSelectionModel, RegattaRefresher regattaRefresher, + public StructureImportListComposite(final SailingServiceAsync sailingService, RegattaRefresher regattaRefresher, RegattaStructureProvider regattaStructureProvider, final ErrorReporter errorReporter, final StringMessages stringMessages) { - super(sailingService, refreshableRegattaSelectionModel, regattaRefresher, errorReporter, stringMessages); + super(sailingService, regattaRefresher, errorReporter, stringMessages); this.regattaStructureProvider = regattaStructureProvider; } // create Regatta Table in StructureImportManagementPanel @Override - protected CellTable createRegattaTable(RefreshableMultiSelectionModel refreshableRegattaSelectionModel) { + protected CellTable createRegattaTable() { CellTable table = new CellTable(/* pageSize */10000, tableRes); regattaListDataProvider.addDataDisplay(table); table.setWidth("100%"); - this.selectionCheckboxColumn = new SelectionCheckboxColumn(refreshableRegattaSelectionModel, + this.selectionCheckboxColumn = new SelectionCheckboxColumn( tableRes.cellTableStyle().cellTableCheckboxSelected(), tableRes.cellTableStyle().cellTableCheckboxDeselected(), - tableRes.cellTableStyle().cellTableCheckboxColumnCell()) { + tableRes.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { + @Override + public boolean representSameEntity(RegattaDTO dto1, RegattaDTO dto2) { + return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); + } + }) { @Override protected ListDataProvider getListDataProvider() { return regattaListDataProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java index b005ad38433..2c05ef7d624 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java @@ -45,8 +45,6 @@ import com.sap.sailing.gwt.ui.shared.LeaderboardGroupDTO; import com.sap.sailing.gwt.ui.shared.RegattaDTO; import com.sap.sailing.gwt.ui.shared.SeriesDTO; import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; -import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.controls.busyindicator.BusyIndicator; import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; @@ -104,14 +102,8 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat this.errorReporter = errorReporter; this.stringMessages = stringMessages; this.regattaRefresher = regattaRefresher; - this.regattaListComposite = new StructureImportListComposite(this.sailingService, - new RefreshableMultiSelectionModel(new EntityIdentityComparator() { - - @Override - public boolean representSameEntity(RegattaDTO dto1, RegattaDTO dto2) { - return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); - } - }), this.regattaRefresher, this, this.errorReporter, this.stringMessages); + this.regattaListComposite = new StructureImportListComposite(this.sailingService, this.regattaRefresher, this, + this.errorReporter, this.stringMessages); regattaListComposite.ensureDebugId("RegattaListComposite"); VerticalPanel mainPanel = new VerticalPanel(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java index 1315019f7cc..3ed44bc3749 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java @@ -1,7 +1,6 @@ package com.sap.sailing.gwt.ui.client.shared.controls; import java.util.List; - import com.google.gwt.cell.client.Cell.Context; import com.google.gwt.dom.client.BrowserEvents; import com.google.gwt.dom.client.NativeEvent; @@ -21,7 +20,6 @@ import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; import com.sap.sailing.domain.common.impl.InvertibleComparatorAdapter; import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; -import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; /** * A column to be used in a {@link CellTable} that controls and reflects a table's selection model using stylable @@ -50,23 +48,7 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW private final String checkboxColumnCellCSSClass; private final EventTranslator selectionEventTranslator; private final RefreshableMultiSelectionModel selectionModel; - - /** - * @param selectionModel - * {@link RefreshableSelectionModel} to set an already existing selectionmodel for the - * {@link SelectionCheckboxColumn} - * @param selectedCheckboxCSSClass - * CSS class for the <div> element representing a selected element - * @param deselectedCheckboxCSSClass - * CSS class for the <div> element representing a deselected element - * @param checkboxColumnCellCSSClass - * CSS class for the <td> element rendering the cell - */ - // TODO Lukas: test whether using an arbitrary selection model here that does not trigger the redrawRow method displays the checkbox correctly when the selection is changed programmatically; is this required at all, or could the client grab the selection model from this column instead? - protected SelectionCheckboxColumn(RefreshableMultiSelectionModel selectionModel, String selectedCheckboxCSSClass, - String deselectedCheckboxCSSClass, String checkboxColumnCellCSSClass) { - this(selectionModel, new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass); - } + /** * @param selectedCheckboxCSSClass * CSS class for the <div> element representing a selected element @@ -82,15 +64,6 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW this(new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass, entityIdentityComparator); } - - // TODO Lukas: test whether using an arbitrary selection model here that does not trigger the redrawRow method displays the checkbox correctly when the selection is changed programmatically; is this required at all, or could the client grab the selection model from this column instead? - private SelectionCheckboxColumn(RefreshableMultiSelectionModel selectionModel, BetterCheckboxCell checkboxCell, String checkboxColumnCellCSSClass) { - super(checkboxCell, SortingOrder.DESCENDING); - this.cell = checkboxCell; - this.checkboxColumnCellCSSClass = checkboxColumnCellCSSClass; - this.selectionEventTranslator = createSelectionEventTranslator(); - this.selectionModel = selectionModel; - } private SelectionCheckboxColumn(BetterCheckboxCell checkboxCell, String checkboxColumnCellCSSClass, EntityIdentityComparator entityIdentityComparator) { super(checkboxCell, SortingOrder.DESCENDING); From 61f782c6ca9dba6c39eb5f1e198c93501a081809 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 2 Dec 2015 15:56:34 +0100 Subject: [PATCH 088/306] newSelectionModel: remove the constructor with out EntityIdentityComparator from RefreshableSingleSelectionModenewSelectionModel: remove the constructor with out EntityIdentityComparator from RefreshableSingleSelectionModell --- .../sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java | 2 +- .../com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java | 8 +++++++- .../client/celltable/RefreshableSingleSelectionModel.java | 4 ---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java index 7326be8ef8e..f1080c42761 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java @@ -76,7 +76,7 @@ public class IgtimiAccountsPanel extends FlowPanel { AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); allAccounts = new CellTable(/* pageSize */10000, tableRes); - refreshableAccountsSelectionModel = new RefreshableSingleSelectionModel(); + refreshableAccountsSelectionModel = new RefreshableSingleSelectionModel(null); allAccounts.setSelectionModel(refreshableAccountsSelectionModel); final ListDataProvider filteredAccounts = new ListDataProvider(); ListHandler accountColumnListHandler = new ListHandler(filteredAccounts.getList()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java index 5704ba698d4..ee9b916908c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java @@ -52,6 +52,7 @@ import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsTimePoint; 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.RefreshableSingleSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -166,7 +167,12 @@ public class MediaPanel extends FlowPanel { mediaTracksTable.addColumnSortHandler(sortHandler); // Add a selection model so we can select cells. - refreshableSelectionModel = new RefreshableSingleSelectionModel<>(); + refreshableSelectionModel = new RefreshableSingleSelectionModel<>(new EntityIdentityComparator() { + @Override + public boolean representSameEntity(MediaTrack dto1, MediaTrack dto2) { + return dto1.dbId.equals(dto2.dbId); + } + }); mediaTracksTable.setSelectionModel(refreshableSelectionModel, DefaultSelectionEventManager. createDefaultManager()); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index f513407da1a..d5d135358b6 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -12,10 +12,6 @@ import com.google.gwt.view.client.SingleSelectionModel; public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel { private final EntityIdentityComparator comp; - public RefreshableSingleSelectionModel() { - super(); - comp = null; - } /** * TODO Lukas: Add Javadoc */ From f820734ec213ed1c31d7435dcba5e8d1939ed19a Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 2 Dec 2015 16:02:10 +0100 Subject: [PATCH 089/306] newSelectionModel: removed the constructor with out the EntityIdentityComparator from RefreshableMultiSelctionModel --- .../gwt/ui/adminconsole/DeviceConfigurationPanel.java | 2 +- .../client/celltable/RefreshableMultiSelectionModel.java | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java index 92a709c8dd4..9ac8791a72a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java @@ -56,7 +56,7 @@ public class DeviceConfigurationPanel extends SimplePanel implements DeviceConfi this.stringMessages = stringMessages; this.errorReporter = reporter; - this.refreshableMultiSelectionModel = new RefreshableMultiSelectionModel<>(); + this.refreshableMultiSelectionModel = new RefreshableMultiSelectionModel<>(null); //TODO create an appropriate EntityIdentityComparator or define an .eqauls() method for DeviceConfigurationMatcherDTO refreshableMultiSelectionModel.addSelectionChangeHandler(new Handler() { diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 6d41fdd3af2..00bbec48ca3 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -13,12 +13,7 @@ import com.google.gwt.view.client.SelectionChangeEvent; */ public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel { private final EntityIdentityComparator comp; - - public RefreshableMultiSelectionModel() { - super(); - comp = null; - } - + /** * TODO Lukas: Add Javadoc */ From 20c226930d3c575296a136de0e56439182506810 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 2 Dec 2015 17:17:08 +0100 Subject: [PATCH 090/306] Added StringMessages for meta leaderboard live text --- .../java/com/sap/sailing/gwt/ui/client/StringMessages.java | 2 ++ .../com/sap/sailing/gwt/ui/client/StringMessages.properties | 2 ++ .../sap/sailing/gwt/ui/client/StringMessages_de.properties | 2 ++ .../sap/sailing/gwt/ui/client/StringMessages_ru.properties | 2 ++ .../sap/sailing/gwt/ui/client/StringMessages_zh.properties | 4 +++- 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 5c82ce097aa..aef56b3dea5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -503,6 +503,8 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, c String sideToWhichMarkAtLegStartWasRounded(); String raceIsLive(String raceName); String racesAreLive(String raceNames); + String regattaIsLive(String regattaName); + String regattasAreLive(String regattaNames); String scoringSchemeHighPointFirstGetsOne(); String scoringSchemeHighPointFirstGetsTen(); String knotsUnit(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 7c9b921b440..f6e4d10a7a4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -527,6 +527,8 @@ sideToWhichMarkAtLegStartWasRounded=Mark rounded to sideToWhichMarkAtLegStartWasRoundedTooltip=Side to which the mark at leg start was rounded. raceIsLive=Race {0} is live racesAreLive=Races {0} are live +regattaIsLive=Regatta {0} is live +regattasAreLive=Regattas {0} are live knotsUnit=kts knotsValue={0,number,#0.0} kts knotsRange={0,number,#0.0} - {1,number,#0.0} kts diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index 189a589b9f1..3b2e97f5e2a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -528,6 +528,8 @@ sideToWhichMarkAtLegStartWasRounded=Tonne gerundet an sideToWhichMarkAtLegStartWasRoundedTooltip=Seite, an der die Bahnmarke am Schenkelstart gerundet wurde raceIsLive=Rennen {0} ist live racesAreLive=Rennen {0} sind live +regattaIsLive=Regatta {0} ist live +regattasAreLive=Regatten {0} sind live scoringSchemeHighPointFirstGetsOne=High Point System, Gewinner erhält 1 Punkt scoringSchemeHighPointFirstGetsTen=High Point System, Gewinner erhält 10 Punkte knotsUnit=kn diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index c1a452bacc9..b7533df02b7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -511,6 +511,8 @@ sideToWhichMarkAtLegStartWasRounded=Отметка округленная до sideToWhichMarkAtLegStartWasRoundedTooltip=Сторона в которую округляется отметка на старте этапа. raceIsLive=Гонка {0} в прямом эфире racesAreLive=Гонки {0} в прямом эфире +regattaIsLive=Регата {0} в прямом эфире +regattasAreLive=Регаты {0} в прямом эфире knotsUnit=Узлы knotsValue={0,number,#0.0} Узлы knotsRange={0,number,#0.0} - {1,number,#0.0} Узлы diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index c8aa38e7f9c..3b5ff10322a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -844,7 +844,10 @@ explainReplicationHostname=Hostname where the replication master is running on. igtimiAccounts=Igtimi Accounts windChart=Wind Chart events=活动 +raceIsLive=Race {0} is live racesAreLive=Races {0} are live +regattaIsLive=Regatta {0} is live +regattasAreLive=Regattas {0} are live operatorNotContains=Not Contains windwardDistanceToCompetitorFarthestAheadInMetersTooltip=迎/顺风航段时离开上风/下风第一名的平行距离或横风时的实际距离name 名字 errorAddingResultImportUrl=Error adding result import URL: {0} @@ -1066,7 +1069,6 @@ polarSheetMinimumConfidenceMeasure=Minimum confidence measure operator=operator tractracUsername=TracTrac username webWindSourceTypeName=Manual -raceIsLive=Race {0} is live sapSailingAnalytics=航行分析 trackWind=Track Wind rawFixes=All fixes (including outliers) From c5c74bb3fc1ee75939dace5ad892926a3029284e Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 2 Dec 2015 17:22:33 +0100 Subject: [PATCH 091/306] fixed a nullPointerExeption --- .../sap/sailing/gwt/ui/adminconsole/EventListComposite.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 45c75799d84..61df37f985d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -63,7 +63,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le private final StringMessages stringMessages; private CellTable eventTable; - private RefreshableMultiSelectionModel refreshableEventSelectionModel; + private final RefreshableMultiSelectionModel refreshableEventSelectionModel; private ListDataProvider eventListDataProvider; private List allEvents; private LabeledAbstractFilterablePanel filterTextbox; @@ -91,6 +91,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le private final AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); + @SuppressWarnings("unchecked") public EventListComposite(final SailingServiceAsync sailingService, final ErrorReporter errorReporter, final StringMessages stringMessages) { this.sailingService = sailingService; this.stringMessages = stringMessages; @@ -139,6 +140,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le eventListDataProvider = new ListDataProvider(); eventTable = createEventTable(); eventTable.ensureDebugId("EventsCellTable"); + refreshableEventSelectionModel = (RefreshableMultiSelectionModel) eventTable.getSelectionModel(); eventTable.setVisible(false); this.refreshableEventSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { From f4686bbe852a4722f114900f457d07b85d44ad81 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 2 Dec 2015 17:56:12 +0100 Subject: [PATCH 092/306] Added LeaderboardType calculation to set it in LeaderboardDTOs --- .../impl/AbstractSimpleLeaderboardImpl.java | 5 + .../impl/FlexibleLeaderboardImpl.java | 678 +++++++++--------- .../impl/RegattaLeaderboardImpl.java | 240 ++++--- .../meta/FlexibleMetaLeaderboard.java | 134 ++-- .../meta/LeaderboardGroupMetaLeaderboard.java | 190 ++--- 5 files changed, 638 insertions(+), 609 deletions(-) diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java index bdf437fe905..08d5fe903d9 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java @@ -42,6 +42,7 @@ import com.sap.sailing.domain.base.RaceColumnInSeries; import com.sap.sailing.domain.base.RaceColumnListener; import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.LeaderboardType; import com.sap.sailing.domain.common.LegType; import com.sap.sailing.domain.common.ManeuverType; import com.sap.sailing.domain.common.MaxPointsReason; @@ -1218,6 +1219,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race this.getScoreCorrection() == null ? null : this.getScoreCorrection().getComment(), this.getScoringScheme() == null ? null : this.getScoringScheme().getType(), this .getScoringScheme().isHigherBetter(), new UUIDGenerator(), addOverallDetails); + result.type = getLeaderboardType(); result.competitors = new ArrayList(); result.name = this.getName(); result.competitorDisplayNames = new HashMap(); @@ -1929,4 +1931,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race public NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher() { return new NumberOfCompetitorsFetcherImpl(); } + + protected abstract LeaderboardType getLeaderboardType(); + } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java index ab37c03d57d..62d50860e6a 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java @@ -1,336 +1,342 @@ -package com.sap.sailing.domain.leaderboard.impl; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.ObjectStreamException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Logger; - -import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.base.impl.AbstractRaceExecutionOrderProvider; -import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; -import com.sap.sailing.domain.leaderboard.FlexibleRaceColumn; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.racelog.RaceLogStore; -import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; -import com.sap.sailing.domain.regattalike.BaseRegattaLikeImpl; -import com.sap.sailing.domain.regattalike.FlexibleLeaderboardAsRegattaLikeIdentifier; -import com.sap.sailing.domain.regattalike.IsRegattaLike; -import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; -import com.sap.sailing.domain.regattalike.RegattaLikeListener; -import com.sap.sailing.domain.regattalog.RegattaLogStore; -import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore; -import com.sap.sailing.domain.tracking.RaceExecutionOrderProvider; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sse.common.Duration; - -/** - * A leaderboard implementation that allows users to flexibly configure which columns exist. No constraints need to be observed regarding - * the columns belonging to the same regatta or even boat class.

    - * - * The flexible leaderboard listens as {@link RaceColumnListener} on all its {@link RaceColumn}s and forwards all events to - * all {@link RaceColumnListener}s subscribed with this leaderboard. - * - * @author Axel Uhl (D043530) - * - */ -public class FlexibleLeaderboardImpl extends AbstractLeaderboardImpl implements FlexibleLeaderboard { - private static Logger logger = Logger.getLogger(FlexibleLeaderboardImpl.class.getName()); - - protected static final DefaultFleetImpl defaultFleet = new DefaultFleetImpl(); - private static final long serialVersionUID = -5708971849158747846L; - private final List races; - private final ScoringScheme scoringScheme; - private String name; - private transient RaceLogStore raceLogStore; - private CourseArea courseArea; - private RaceExecutionOrderProvider raceExecutionOrderProvider; - - /** - * @see RegattaLog for the reason why the leaderboard manages a {@code RegattaLog} - */ - private final IsRegattaLike regattaLikeHelper; - - public FlexibleLeaderboardImpl(String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, - ScoringScheme scoringScheme, CourseArea courseArea) { - this(EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, - name, resultDiscardingRule, scoringScheme, courseArea); - } - - public FlexibleLeaderboardImpl(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, - String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, - ScoringScheme scoringScheme, CourseArea courseArea) { - super(resultDiscardingRule); - this.scoringScheme = scoringScheme; - if (name == null) { - throw new IllegalArgumentException("A leaderboard's name must not be null"); - } - this.name = name; - this.races = new ArrayList<>(); - this.raceLogStore = raceLogStore; - this.courseArea = courseArea; - this.regattaLikeHelper = new BaseRegattaLikeImpl(new FlexibleLeaderboardAsRegattaLikeIdentifier(this), regattaLogStore) { - private static final long serialVersionUID = 4082392360832548953L; - - @Override - public RaceColumn getRaceColumnByName(String raceColumnName) { - return getRaceColumnByName(raceColumnName); - } - }; - this.raceExecutionOrderProvider = new RaceExecutionOrderCache(); - } - - /** - * Deserialization has to be maintained in lock-step with {@link #writeObject(ObjectOutputStream) serialization}. - * When de-serializing, a possibly remote {@link #raceLogStore} is ignored because it is transient. Instead, an - * {@link EmptyRaceLogStore} is used for the de-serialized instance. A new {@link RaceLogInformation} is - * assembled for this empty race log and applied to all columns. - */ - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - ois.defaultReadObject(); - raceLogStore = EmptyRaceLogStore.INSTANCE; - for (RaceColumn column : getRaceColumns()) { - column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); - } - } - - protected Object readResolve() throws ObjectStreamException { - raceExecutionOrderProvider.triggerUpdate(); - return this; - } - - @Override - public String getName() { - return name; - } - - /** - * @param newName must not be null - */ - public void setName(String newName) { - if (newName == null) { - throw new IllegalArgumentException("A leaderboard's name must not be null"); - } - this.name = newName; - } - - @Override - public RaceColumn addRace(TrackedRace race, String columnName, boolean medalRace) { - FlexibleRaceColumn column = addRaceColumn(columnName, medalRace, /* logAlreadyExistingColumn */false); - column.setTrackedRace(defaultFleet, race); // triggers listeners because this object was registered above as - // race column listener on the column - return column; - } - - @Override - public FlexibleRaceColumn addRaceColumn(String name, boolean medalRace) { - return addRaceColumn(name, medalRace, /* logAlreadyExistingColumn */true); - } - - private FlexibleRaceColumn addRaceColumn(String name, boolean medalRace, boolean logAlreadyExistingColumn) { - FlexibleRaceColumn column = getRaceColumnByName(name); - if (column != null) { - if (logAlreadyExistingColumn) { - final String msg = "Trying to create race column with duplicate name " + name + " in leaderboard " + getName(); - logger.severe(msg); - } - } else { - column = createRaceColumn(name, medalRace); - column.addRaceColumnListener(this); - races.add(column); - column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(column); - } - return column; - } - - @Override - public FlexibleRaceColumn getRaceColumnByName(String columnName) { - return (FlexibleRaceColumn) super.getRaceColumnByName(columnName); - } - - @Override - public Fleet getFleet(String fleetName) { - Fleet result; - if (fleetName == null) { - result = defaultFleet; - } else { - result = super.getFleet(fleetName); - } - return result; - } - - @Override - public void removeRaceColumn(String columnName) { - final FlexibleRaceColumn raceColumn = getRaceColumnByName(columnName); - if (raceColumn != null) { - for (Fleet fleet : raceColumn.getFleets()) { - raceLogStore.removeRaceLog(raceColumn.getRaceLogIdentifier(fleet)); - if (raceColumn.getTrackedRace(fleet) != null) { - raceColumn.getTrackedRace(fleet).detachRaceExecutionOrderProvider(raceExecutionOrderProvider); - } - } - races.remove(raceColumn); - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); - raceColumn.removeRaceColumnListener(this); - } - } - - @Override - public Iterable getRaceColumns() { - final Iterable result; - if (races != null) { - result = Collections.unmodifiableCollection(new ArrayList(races)); - } else { - result = null; - } - return result; - } - - protected RaceColumnImpl createRaceColumn(String column, boolean medalRace) { - return new RaceColumnImpl(column, medalRace, raceExecutionOrderProvider); - } - - protected Iterable turnNullOrEmptyFleetsIntoDefaultFleet(Fleet... fleets) { - Iterable theFleets; - if (fleets == null || fleets.length == 0) { - Fleet defaultfleetCasted = defaultFleet; - theFleets = Collections.singleton(defaultfleetCasted); - } else { - theFleets = Arrays.asList(fleets); - } - return theFleets; - } - - @Override - public void moveRaceColumnUp(String name) { - FlexibleRaceColumn race = null; - for (FlexibleRaceColumn r : races) { - if (r.getName().equals(name)) { - race = r; - } - } - if (race != null) { - int index = 0; - index = races.lastIndexOf(race); - index--; - if (index >= 0) { - races.remove(race); - races.add(index, race); - getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); - } - } - } - - @Override - public void moveRaceColumnDown(String name) { - FlexibleRaceColumn race = null; - for (FlexibleRaceColumn r : races) { - if (r.getName().equals(name)) { - race = r; - } - } - if (race != null) { - int index = 0; - index = races.lastIndexOf(race); - if (index != -1) { - index++; - if (index < races.size()) { - races.remove(race); - races.add(index, race); - getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); - } - } - } - } - - @Override - public void updateIsMedalRace(String raceName, boolean isMedalRace) { - FlexibleRaceColumn race = null; - for (FlexibleRaceColumn r : races) { - if (r.getName().equals(raceName)) - race = r; - } - if (race != null) { - race.setIsMedalRace(isMedalRace); - } - } - - @Override - public ScoringScheme getScoringScheme() { - return scoringScheme; - } - - @Override - public CourseArea getDefaultCourseArea() { - return courseArea; - } - - @Override - public void setDefaultCourseArea(CourseArea newCourseArea) { - this.courseArea = newCourseArea; - } - - @Override - public IsRegattaLike getRegattaLike() { - return regattaLikeHelper; - } - - @Override - public RegattaLog getRegattaLog() { - return regattaLikeHelper.getRegattaLog(); - } - - @Override - public RegattaLikeIdentifier getRegattaLikeIdentifier() { - return regattaLikeHelper.getRegattaLikeIdentifier(); - } - - @Override - public void addListener(RegattaLikeListener listener) { - regattaLikeHelper.addListener(listener); - } - - @Override - public void removeListener(RegattaLikeListener listener) { - regattaLikeHelper.removeListener(listener); - } - - @Override - public Double getTimeOnTimeFactor(Competitor competitor) { - return regattaLikeHelper.getTimeOnTimeFactor(competitor); - } - - @Override - public Duration getTimeOnDistanceAllowancePerNauticalMile(Competitor competitor) { - return regattaLikeHelper.getTimeOnDistanceAllowancePerNauticalMile(competitor); - } - - private class RaceExecutionOrderCache extends AbstractRaceExecutionOrderProvider { - private static final long serialVersionUID = 652833386555762661L; - - public RaceExecutionOrderCache() { - super(); - addRaceColumnListener(this); - } - - @Override - protected Map> getRaceColumnsOfSeries() { - final Map> result = new HashMap<>(); - result.put(FlexibleLeaderboardImpl.defaultFleet, getRaceColumns()); - return result; - } - } -} +package com.sap.sailing.domain.leaderboard.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.impl.AbstractRaceExecutionOrderProvider; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.FlexibleRaceColumn; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.regattalike.BaseRegattaLikeImpl; +import com.sap.sailing.domain.regattalike.FlexibleLeaderboardAsRegattaLikeIdentifier; +import com.sap.sailing.domain.regattalike.IsRegattaLike; +import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; +import com.sap.sailing.domain.regattalike.RegattaLikeListener; +import com.sap.sailing.domain.regattalog.RegattaLogStore; +import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore; +import com.sap.sailing.domain.tracking.RaceExecutionOrderProvider; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.Duration; + +/** + * A leaderboard implementation that allows users to flexibly configure which columns exist. No constraints need to be observed regarding + * the columns belonging to the same regatta or even boat class.

    + * + * The flexible leaderboard listens as {@link RaceColumnListener} on all its {@link RaceColumn}s and forwards all events to + * all {@link RaceColumnListener}s subscribed with this leaderboard. + * + * @author Axel Uhl (D043530) + * + */ +public class FlexibleLeaderboardImpl extends AbstractLeaderboardImpl implements FlexibleLeaderboard { + private static Logger logger = Logger.getLogger(FlexibleLeaderboardImpl.class.getName()); + + protected static final DefaultFleetImpl defaultFleet = new DefaultFleetImpl(); + private static final long serialVersionUID = -5708971849158747846L; + private final List races; + private final ScoringScheme scoringScheme; + private String name; + private transient RaceLogStore raceLogStore; + private CourseArea courseArea; + private RaceExecutionOrderProvider raceExecutionOrderProvider; + + /** + * @see RegattaLog for the reason why the leaderboard manages a {@code RegattaLog} + */ + private final IsRegattaLike regattaLikeHelper; + + public FlexibleLeaderboardImpl(String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, + ScoringScheme scoringScheme, CourseArea courseArea) { + this(EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, + name, resultDiscardingRule, scoringScheme, courseArea); + } + + public FlexibleLeaderboardImpl(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, + ScoringScheme scoringScheme, CourseArea courseArea) { + super(resultDiscardingRule); + this.scoringScheme = scoringScheme; + if (name == null) { + throw new IllegalArgumentException("A leaderboard's name must not be null"); + } + this.name = name; + this.races = new ArrayList<>(); + this.raceLogStore = raceLogStore; + this.courseArea = courseArea; + this.regattaLikeHelper = new BaseRegattaLikeImpl(new FlexibleLeaderboardAsRegattaLikeIdentifier(this), regattaLogStore) { + private static final long serialVersionUID = 4082392360832548953L; + + @Override + public RaceColumn getRaceColumnByName(String raceColumnName) { + return getRaceColumnByName(raceColumnName); + } + }; + this.raceExecutionOrderProvider = new RaceExecutionOrderCache(); + } + + /** + * Deserialization has to be maintained in lock-step with {@link #writeObject(ObjectOutputStream) serialization}. + * When de-serializing, a possibly remote {@link #raceLogStore} is ignored because it is transient. Instead, an + * {@link EmptyRaceLogStore} is used for the de-serialized instance. A new {@link RaceLogInformation} is + * assembled for this empty race log and applied to all columns. + */ + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + raceLogStore = EmptyRaceLogStore.INSTANCE; + for (RaceColumn column : getRaceColumns()) { + column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); + } + } + + protected Object readResolve() throws ObjectStreamException { + raceExecutionOrderProvider.triggerUpdate(); + return this; + } + + @Override + public String getName() { + return name; + } + + /** + * @param newName must not be null + */ + public void setName(String newName) { + if (newName == null) { + throw new IllegalArgumentException("A leaderboard's name must not be null"); + } + this.name = newName; + } + + @Override + public RaceColumn addRace(TrackedRace race, String columnName, boolean medalRace) { + FlexibleRaceColumn column = addRaceColumn(columnName, medalRace, /* logAlreadyExistingColumn */false); + column.setTrackedRace(defaultFleet, race); // triggers listeners because this object was registered above as + // race column listener on the column + return column; + } + + @Override + public FlexibleRaceColumn addRaceColumn(String name, boolean medalRace) { + return addRaceColumn(name, medalRace, /* logAlreadyExistingColumn */true); + } + + private FlexibleRaceColumn addRaceColumn(String name, boolean medalRace, boolean logAlreadyExistingColumn) { + FlexibleRaceColumn column = getRaceColumnByName(name); + if (column != null) { + if (logAlreadyExistingColumn) { + final String msg = "Trying to create race column with duplicate name " + name + " in leaderboard " + getName(); + logger.severe(msg); + } + } else { + column = createRaceColumn(name, medalRace); + column.addRaceColumnListener(this); + races.add(column); + column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(column); + } + return column; + } + + @Override + public FlexibleRaceColumn getRaceColumnByName(String columnName) { + return (FlexibleRaceColumn) super.getRaceColumnByName(columnName); + } + + @Override + public Fleet getFleet(String fleetName) { + Fleet result; + if (fleetName == null) { + result = defaultFleet; + } else { + result = super.getFleet(fleetName); + } + return result; + } + + @Override + public void removeRaceColumn(String columnName) { + final FlexibleRaceColumn raceColumn = getRaceColumnByName(columnName); + if (raceColumn != null) { + for (Fleet fleet : raceColumn.getFleets()) { + raceLogStore.removeRaceLog(raceColumn.getRaceLogIdentifier(fleet)); + if (raceColumn.getTrackedRace(fleet) != null) { + raceColumn.getTrackedRace(fleet).detachRaceExecutionOrderProvider(raceExecutionOrderProvider); + } + } + races.remove(raceColumn); + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); + raceColumn.removeRaceColumnListener(this); + } + } + + @Override + public Iterable getRaceColumns() { + final Iterable result; + if (races != null) { + result = Collections.unmodifiableCollection(new ArrayList(races)); + } else { + result = null; + } + return result; + } + + protected RaceColumnImpl createRaceColumn(String column, boolean medalRace) { + return new RaceColumnImpl(column, medalRace, raceExecutionOrderProvider); + } + + protected Iterable turnNullOrEmptyFleetsIntoDefaultFleet(Fleet... fleets) { + Iterable theFleets; + if (fleets == null || fleets.length == 0) { + Fleet defaultfleetCasted = defaultFleet; + theFleets = Collections.singleton(defaultfleetCasted); + } else { + theFleets = Arrays.asList(fleets); + } + return theFleets; + } + + @Override + public void moveRaceColumnUp(String name) { + FlexibleRaceColumn race = null; + for (FlexibleRaceColumn r : races) { + if (r.getName().equals(name)) { + race = r; + } + } + if (race != null) { + int index = 0; + index = races.lastIndexOf(race); + index--; + if (index >= 0) { + races.remove(race); + races.add(index, race); + getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); + } + } + } + + @Override + public void moveRaceColumnDown(String name) { + FlexibleRaceColumn race = null; + for (FlexibleRaceColumn r : races) { + if (r.getName().equals(name)) { + race = r; + } + } + if (race != null) { + int index = 0; + index = races.lastIndexOf(race); + if (index != -1) { + index++; + if (index < races.size()) { + races.remove(race); + races.add(index, race); + getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); + } + } + } + } + + @Override + public void updateIsMedalRace(String raceName, boolean isMedalRace) { + FlexibleRaceColumn race = null; + for (FlexibleRaceColumn r : races) { + if (r.getName().equals(raceName)) + race = r; + } + if (race != null) { + race.setIsMedalRace(isMedalRace); + } + } + + @Override + public ScoringScheme getScoringScheme() { + return scoringScheme; + } + + @Override + public CourseArea getDefaultCourseArea() { + return courseArea; + } + + @Override + public void setDefaultCourseArea(CourseArea newCourseArea) { + this.courseArea = newCourseArea; + } + + @Override + public IsRegattaLike getRegattaLike() { + return regattaLikeHelper; + } + + @Override + public RegattaLog getRegattaLog() { + return regattaLikeHelper.getRegattaLog(); + } + + @Override + public RegattaLikeIdentifier getRegattaLikeIdentifier() { + return regattaLikeHelper.getRegattaLikeIdentifier(); + } + + @Override + public void addListener(RegattaLikeListener listener) { + regattaLikeHelper.addListener(listener); + } + + @Override + public void removeListener(RegattaLikeListener listener) { + regattaLikeHelper.removeListener(listener); + } + + @Override + public Double getTimeOnTimeFactor(Competitor competitor) { + return regattaLikeHelper.getTimeOnTimeFactor(competitor); + } + + @Override + public Duration getTimeOnDistanceAllowancePerNauticalMile(Competitor competitor) { + return regattaLikeHelper.getTimeOnDistanceAllowancePerNauticalMile(competitor); + } + + private class RaceExecutionOrderCache extends AbstractRaceExecutionOrderProvider { + private static final long serialVersionUID = 652833386555762661L; + + public RaceExecutionOrderCache() { + super(); + addRaceColumnListener(this); + } + + @Override + protected Map> getRaceColumnsOfSeries() { + final Map> result = new HashMap<>(); + result.put(FlexibleLeaderboardImpl.defaultFleet, getRaceColumns()); + return result; + } + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.FlexibleLeaderboard; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java index a6df854049f..d5af239cf6a 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java @@ -1,117 +1,123 @@ -package com.sap.sailing.domain.leaderboard.impl; - -import java.util.ArrayList; -import java.util.List; - -import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnInSeries; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.impl.RaceColumnInSeriesImpl; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.regattalike.IsRegattaLike; -import com.sap.sailing.domain.tracking.TrackedRace; - -/** - * A leaderboard that is based on the definition of a {@link Regatta} with its {@link Series} and {@link Fleet}. The regatta - * leaderboard listens to its {@link Regatta} as a {@link RaceColumnListener} and forwards all link/unlink events for - * {@link TrackedRace}s being linked to / unlinked from race columns to all {@link RaceColumnListener}s subscribed with - * this leaderboard. - * - * @author Axel Uhl (D043530) - * - */ -public class RegattaLeaderboardImpl extends AbstractLeaderboardImpl implements RegattaLeaderboard { - private static final long serialVersionUID = 2370461218294770084L; - private final Regatta regatta; - - public RegattaLeaderboardImpl(Regatta regatta, ThresholdBasedResultDiscardingRule resultDiscardingRule) { - super(resultDiscardingRule); - this.regatta = regatta; - regatta.addRaceColumnListener(this); - } - - /** - * Updates the display name of this regatta leaderboard so that the regatta's name is no longer used as the default name. - */ - @Override - public void setName(String newName) { - setDisplayName(newName); - } - - @Override - public Regatta getRegatta() { - return regatta; - } - - @Override - public String getName() { - return getRegatta().getName(); - } - - @Override - public Iterable getRaceColumns() { - List result = new ArrayList(); - for (Series series : getRegatta().getSeries()) { - for (RaceColumn raceColumn : series.getRaceColumns()) { - result.add(raceColumn); - } - } - return result; - } - - @Override - public RaceColumnInSeries getRaceColumnByName(String columnName) { - return (RaceColumnInSeriesImpl) super.getRaceColumnByName(columnName); - } - - @Override - public ScoringScheme getScoringScheme() { - return regatta.getScoringScheme(); - } - - @Override - public CourseArea getDefaultCourseArea() { - return regatta.getDefaultCourseArea(); - } - - /** - * If the regatta' series {@link Regatta#definesSeriesDiscardThresholds() define} their own result discarding rules, this leaderboard uses - * a composite result discarding rule of type {@link PerSeriesResultDiscardingRuleImpl} that contains discards within each series according - * to the series' discarding rules. Otherwise, the default cross-leaderboard result discarding rule is obtained from the super class - * implementation. - */ - @Override - public ResultDiscardingRule getResultDiscardingRule() { - if (regatta.definesSeriesDiscardThresholds()) { - return new PerSeriesResultDiscardingRuleImpl(regatta); - } else { - return super.getResultDiscardingRule(); - } - } - - /** - * Delegates to {@link Regatta#getAllCompetitors()} which is expected to deliver all competitors from this - * leaderboard's regatta which includes those belonging to {@link RaceDefinition races} - * {@link Regatta#getAllRaces() belonging to the regatta} as well as competitors listed on the regatta's - * {@link RegattaLog}. - */ - @Override - public Iterable getAllCompetitors() { - return regatta.getAllCompetitors(); - } - - @Override - public IsRegattaLike getRegattaLike() { - return regatta; - } -} +package com.sap.sailing.domain.leaderboard.impl; + +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnInSeries; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.RaceColumnInSeriesImpl; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.regattalike.IsRegattaLike; +import com.sap.sailing.domain.tracking.TrackedRace; + +/** + * A leaderboard that is based on the definition of a {@link Regatta} with its {@link Series} and {@link Fleet}. The regatta + * leaderboard listens to its {@link Regatta} as a {@link RaceColumnListener} and forwards all link/unlink events for + * {@link TrackedRace}s being linked to / unlinked from race columns to all {@link RaceColumnListener}s subscribed with + * this leaderboard. + * + * @author Axel Uhl (D043530) + * + */ +public class RegattaLeaderboardImpl extends AbstractLeaderboardImpl implements RegattaLeaderboard { + private static final long serialVersionUID = 2370461218294770084L; + private final Regatta regatta; + + public RegattaLeaderboardImpl(Regatta regatta, ThresholdBasedResultDiscardingRule resultDiscardingRule) { + super(resultDiscardingRule); + this.regatta = regatta; + regatta.addRaceColumnListener(this); + } + + /** + * Updates the display name of this regatta leaderboard so that the regatta's name is no longer used as the default name. + */ + @Override + public void setName(String newName) { + setDisplayName(newName); + } + + @Override + public Regatta getRegatta() { + return regatta; + } + + @Override + public String getName() { + return getRegatta().getName(); + } + + @Override + public Iterable getRaceColumns() { + List result = new ArrayList(); + for (Series series : getRegatta().getSeries()) { + for (RaceColumn raceColumn : series.getRaceColumns()) { + result.add(raceColumn); + } + } + return result; + } + + @Override + public RaceColumnInSeries getRaceColumnByName(String columnName) { + return (RaceColumnInSeriesImpl) super.getRaceColumnByName(columnName); + } + + @Override + public ScoringScheme getScoringScheme() { + return regatta.getScoringScheme(); + } + + @Override + public CourseArea getDefaultCourseArea() { + return regatta.getDefaultCourseArea(); + } + + /** + * If the regatta' series {@link Regatta#definesSeriesDiscardThresholds() define} their own result discarding rules, this leaderboard uses + * a composite result discarding rule of type {@link PerSeriesResultDiscardingRuleImpl} that contains discards within each series according + * to the series' discarding rules. Otherwise, the default cross-leaderboard result discarding rule is obtained from the super class + * implementation. + */ + @Override + public ResultDiscardingRule getResultDiscardingRule() { + if (regatta.definesSeriesDiscardThresholds()) { + return new PerSeriesResultDiscardingRuleImpl(regatta); + } else { + return super.getResultDiscardingRule(); + } + } + + /** + * Delegates to {@link Regatta#getAllCompetitors()} which is expected to deliver all competitors from this + * leaderboard's regatta which includes those belonging to {@link RaceDefinition races} + * {@link Regatta#getAllRaces() belonging to the regatta} as well as competitors listed on the regatta's + * {@link RegattaLog}. + */ + @Override + public Iterable getAllCompetitors() { + return regatta.getAllCompetitors(); + } + + @Override + public IsRegattaLike getRegattaLike() { + return regatta; + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.RegattaLeaderboard; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java index a7de5eb4c40..e7f9c04de2d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java @@ -1,64 +1,70 @@ -package com.sap.sailing.domain.leaderboard.meta; - -import java.util.ArrayList; -import java.util.List; - -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sse.concurrent.LockUtil; -import com.sap.sse.concurrent.NamedReentrantReadWriteLock; - -public class FlexibleMetaLeaderboard extends AbstractMetaLeaderboard { - private static final long serialVersionUID = 789076326144062944L; - - private final List leaderboards; - - private final NamedReentrantReadWriteLock leaderboardsLock; - - public FlexibleMetaLeaderboard(String name, ScoringScheme scoringScheme, - ThresholdBasedResultDiscardingRule resultDiscardingRule) { - super(name, scoringScheme, resultDiscardingRule); - leaderboards = new ArrayList(); - leaderboardsLock = new NamedReentrantReadWriteLock("leaderboards collection of "+FlexibleMetaLeaderboard.class.getSimpleName()+" "+getName(), - /* fair */ false); - } - - @Override - public Iterable getLeaderboards() { - LockUtil.lockForRead(leaderboardsLock); - try { - return new ArrayList(leaderboards); - } finally { - LockUtil.unlockAfterRead(leaderboardsLock); - } - } - - public void addLeaderboard(Leaderboard leaderboard) { - LockUtil.lockForWrite(leaderboardsLock); - try { - leaderboards.add(leaderboard); - registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); - } finally { - LockUtil.unlockAfterWrite(leaderboardsLock); - } - } - - public void removeLeaderboard(Leaderboard leaderboard) { - LockUtil.lockForWrite(leaderboardsLock); - try { - leaderboards.remove(leaderboard); - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); - unregisterScoreCorrectionChangeForwarder(leaderboard); - } finally { - LockUtil.unlockAfterWrite(leaderboardsLock); - } - } - - @Override - public CourseArea getDefaultCourseArea() { - return null; - } -} +package com.sap.sailing.domain.leaderboard.meta; + +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sse.concurrent.LockUtil; +import com.sap.sse.concurrent.NamedReentrantReadWriteLock; + +public class FlexibleMetaLeaderboard extends AbstractMetaLeaderboard { + private static final long serialVersionUID = 789076326144062944L; + + private final List leaderboards; + + private final NamedReentrantReadWriteLock leaderboardsLock; + + public FlexibleMetaLeaderboard(String name, ScoringScheme scoringScheme, + ThresholdBasedResultDiscardingRule resultDiscardingRule) { + super(name, scoringScheme, resultDiscardingRule); + leaderboards = new ArrayList(); + leaderboardsLock = new NamedReentrantReadWriteLock("leaderboards collection of "+FlexibleMetaLeaderboard.class.getSimpleName()+" "+getName(), + /* fair */ false); + } + + @Override + public Iterable getLeaderboards() { + LockUtil.lockForRead(leaderboardsLock); + try { + return new ArrayList(leaderboards); + } finally { + LockUtil.unlockAfterRead(leaderboardsLock); + } + } + + public void addLeaderboard(Leaderboard leaderboard) { + LockUtil.lockForWrite(leaderboardsLock); + try { + leaderboards.add(leaderboard); + registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); + } finally { + LockUtil.unlockAfterWrite(leaderboardsLock); + } + } + + public void removeLeaderboard(Leaderboard leaderboard) { + LockUtil.lockForWrite(leaderboardsLock); + try { + leaderboards.remove(leaderboard); + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); + unregisterScoreCorrectionChangeForwarder(leaderboard); + } finally { + LockUtil.unlockAfterWrite(leaderboardsLock); + } + } + + @Override + public CourseArea getDefaultCourseArea() { + return null; + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.FlexibleMetaLeaderboard; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java index 73860657959..673e1ee5120 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java @@ -1,92 +1,98 @@ -package com.sap.sailing.domain.leaderboard.meta; - -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.base.impl.TrackedRaces; -import com.sap.sailing.domain.common.LeaderboardNameConstants; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.leaderboard.LeaderboardGroupListener; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.tracking.TrackedRace; - -/** - * A meta leaderboard which considers all regular leaderboards of a {@link LeaderboardGroup}. To stay up to date, this - * meta leaderboard registers itself as a {@link LeaderboardGroupListener} on the leaderboard group it represents. This - * way, whenever a leaderboard is added to or removed from the group, this meta leaderboard can in turn inform its - * {@link RaceColumnListener}s about the impact this change has on the set of {@link TrackedRaces} and therefore the - * competitors attached to this meta leaderboard. - *

    - * - * After an object of this type has been de-serialized, and after all objects referenced by it (leaderboard, leaderboard - * group) have been initialized, {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} must be called to - * ensure that score corrections are propagated as desired. This is because score correction listeners are "transient" - * and are as such not serialized. Note: calling {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} - * during readObject or readResolve is not possible because the object graph hasn't been fully - * initialized yet when those methods are called. Hence, the structures for registering the score correction listeners - * are not yet in place at that time. - * - * @author Axel Uhl (d043530) - * - */ -public class LeaderboardGroupMetaLeaderboard extends AbstractMetaLeaderboard implements LeaderboardGroupListener, RaceColumnListener { - private static final long serialVersionUID = 8087872002175528002L; - - private final LeaderboardGroup leaderboardGroup; - - public LeaderboardGroupMetaLeaderboard(LeaderboardGroup leaderboardGroup, ScoringScheme scoringScheme, - ThresholdBasedResultDiscardingRule resultDiscardingRule) { - super(leaderboardGroup.getName() + " " + LeaderboardNameConstants.OVERALL, scoringScheme, resultDiscardingRule); - this.leaderboardGroup = leaderboardGroup; - leaderboardGroup.addLeaderboardGroupListener(this); - registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards(); - } - - public void registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards() { - for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { - registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); - } - } - - @Override - public Iterable getLeaderboards() { - return leaderboardGroup.getLeaderboards(); - } - - @Override - public void leaderboardAdded(LeaderboardGroup group, Leaderboard leaderboard) { - registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); - } - } - } - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); - } - - @Override - public void leaderboardRemoved(LeaderboardGroup group, Leaderboard leaderboard) { - leaderboard.removeRaceColumnListener(this); - unregisterScoreCorrectionChangeForwarder(leaderboard); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); - } - } - } - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); - } - - @Override - public CourseArea getDefaultCourseArea() { - return null; - } -} +package com.sap.sailing.domain.leaderboard.meta; + +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.impl.TrackedRaces; +import com.sap.sailing.domain.common.LeaderboardNameConstants; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.leaderboard.LeaderboardGroupListener; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.tracking.TrackedRace; + +/** + * A meta leaderboard which considers all regular leaderboards of a {@link LeaderboardGroup}. To stay up to date, this + * meta leaderboard registers itself as a {@link LeaderboardGroupListener} on the leaderboard group it represents. This + * way, whenever a leaderboard is added to or removed from the group, this meta leaderboard can in turn inform its + * {@link RaceColumnListener}s about the impact this change has on the set of {@link TrackedRaces} and therefore the + * competitors attached to this meta leaderboard. + *

    + * + * After an object of this type has been de-serialized, and after all objects referenced by it (leaderboard, leaderboard + * group) have been initialized, {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} must be called to + * ensure that score corrections are propagated as desired. This is because score correction listeners are "transient" + * and are as such not serialized. Note: calling {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} + * during readObject or readResolve is not possible because the object graph hasn't been fully + * initialized yet when those methods are called. Hence, the structures for registering the score correction listeners + * are not yet in place at that time. + * + * @author Axel Uhl (d043530) + * + */ +public class LeaderboardGroupMetaLeaderboard extends AbstractMetaLeaderboard implements LeaderboardGroupListener, RaceColumnListener { + private static final long serialVersionUID = 8087872002175528002L; + + private final LeaderboardGroup leaderboardGroup; + + public LeaderboardGroupMetaLeaderboard(LeaderboardGroup leaderboardGroup, ScoringScheme scoringScheme, + ThresholdBasedResultDiscardingRule resultDiscardingRule) { + super(leaderboardGroup.getName() + " " + LeaderboardNameConstants.OVERALL, scoringScheme, resultDiscardingRule); + this.leaderboardGroup = leaderboardGroup; + leaderboardGroup.addLeaderboardGroupListener(this); + registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards(); + } + + public void registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards() { + for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { + registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); + } + } + + @Override + public Iterable getLeaderboards() { + return leaderboardGroup.getLeaderboards(); + } + + @Override + public void leaderboardAdded(LeaderboardGroup group, Leaderboard leaderboard) { + registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); + } + } + } + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); + } + + @Override + public void leaderboardRemoved(LeaderboardGroup group, Leaderboard leaderboard) { + leaderboard.removeRaceColumnListener(this); + unregisterScoreCorrectionChangeForwarder(leaderboard); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); + } + } + } + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); + } + + @Override + public CourseArea getDefaultCourseArea() { + return null; + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.RegattaMetaLeaderboard; + } +} From 0d9c64ce513805fa9ceae22e9c381bc52191c1c9 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 2 Dec 2015 17:57:02 +0100 Subject: [PATCH 093/306] Determine leaderboard live text based on LeaderboardType --- .../gwt/ui/leaderboard/LeaderboardPanel.java | 6543 ++++++++--------- 1 file changed, 3263 insertions(+), 3280 deletions(-) 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 11c7854898e..0a8679a60d3 100755 --- 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,3280 +1,3263 @@ -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.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.VerticalPanel; -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.InvertibleComparator; -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.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.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.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.leaderboard.LeaderboardSettings.RaceColumnSelectionStrategies; -import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; -import com.sap.sse.common.Util; -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.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.controls.busyindicator.BusyIndicator; -import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; -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.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.useragent.UserAgentDetails; - -/** - * A leaderboard essentially consists of a table widget that in its columns displays the entries. - * - * @author Axel Uhl (D043530) - * - */ -public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayStateListener, DisplayedLeaderboardRowsProvider, - Component, IsEmbeddableComponent, CompetitorSelectionChangeListener, LeaderboardFetcher { - 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"; - - interface RaceColumnTemplates extends SafeHtmlTemplates { - @SafeHtmlTemplates.Template("

    ") - SafeHtml cellFrameWithTextColorAndFleetBorder(String textColor, String borderStyle); - - @SafeHtmlTemplates.Template("
    ") - SafeHtml cellFrameWithTextColor(String textColor); - - @SafeHtmlTemplates.Template("
    ") - SafeHtml cellWithImageResourceAndText(ImageResource image, String text); - } - - private static RaceColumnTemplates raceColumnTemplate = GWT.create(RaceColumnTemplates.class); - - /** - * 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 String leaderboardName; - - private final ErrorReporter errorReporter; - - private final StringMessages stringMessages; - - private final SortedCellTable 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; - - /** - * 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; - - /** - * 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; - - private final List selectedOverallDetailColumns; - - private final Map> overallDetailColumnMap; - - private RaceColumnSelection raceColumnSelection; - - protected final String RACE_COLUMN_HEADER_STYLE; - - protected final String LEG_DETAIL_COLUMN_HEADER_STYLE; - - protected final String LEG_DETAIL_COLUMN_STYLE; - - protected final String LEG_COLUMN_HEADER_STYLE; - - protected final String RACE_COLUMN_STYLE; - - protected final String LEG_COLUMN_STYLE; - - protected final String TOTAL_COLUMN_STYLE; - - 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 VerticalPanel contentPanel; - - private HorizontalPanel refreshAndSettingsPanel; - private Label scoreCorrectionLastUpdateTimeLabel; - private Label scoreCorrectionCommentLabel; - private Label liveRaceLabel; - - private boolean isEmbedded; - - private static final LeaderboardResources resources = GWT.create(LeaderboardResources.class); - private static final ComponentResources componentResources = GWT.create(ComponentResources.class); - private static final LeaderboardTableResources tableResources = GWT.create(LeaderboardTableResources.class); - - private ImageResource pauseIcon; - private ImageResource playIcon; - - private final BusyIndicator busyIndicator; - - /** - * Tells whether the leaderboard settings were explicitly changed by an external call to - * {@link #updateSettings(LeaderboardSettings)}. If so, a {@link #playStateChanged(PlayStates, PlayModes) play state - * change} will not automatically lead to a settings change. - */ - private boolean settingsUpdatedExplicitly = false; - - /** - * Tells if the leaderboard is currently handling a {@link #playStateChanged(PlayStates, PlayModes) play state - * change}. If this is the case, a call to {@link #updateSettings(LeaderboardSettings)} won't set the - * {@link #settingsUpdatedExplicitly} flag. - */ - private boolean currentlyHandlingPlayStateChange; - - private PlayModes oldPlayMode; - - 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 List leaderboardUpdateListener; - - private boolean initialCompetitorFilterHasBeenApplied = false; - private final boolean showCompetitorFilterStatus; - - private CompetitorFilterPanel competitorFilterPanel; - - protected StringMessages getStringMessages() { - return stringMessages; - } - - @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 VerticalPanel getContentPanel() { - return contentPanel; - } - - protected ImageResource getSettingsIcon() { - return componentResources.settingsIcon(); - } - - public void updateSettings(final LeaderboardSettings newSettings) { - boolean oldShallAddOverallDetails = shallAddOverallDetails(); - if (newSettings.getOverallDetailsToShow() != null) { - selectedOverallDetailColumns.clear(); - selectedOverallDetailColumns.addAll(newSettings.getOverallDetailsToShow()); - } - if (!newSettings.isUpdateUponPlayStateChange() || !currentlyHandlingPlayStateChange) { - settingsUpdatedExplicitly = true; - } - 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.toggleExpansion(); - 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()); - } - // 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; - } - - final boolean oldBusyState = getBusyIndicator().isBusy(); - getBusyIndicator().setBusy(true); - Runnable doWhenNecessaryDetailHasBeenLoaded = new Runnable() { - @Override - public void run() { - setAutoExpandPreSelectedRace(false); // avoid expansion during updateLeaderboard(...); will expand later - // if it was expanded before - // update leaderboard after settings panel column selection change - updateLeaderboard(leaderboard); - setAutoExpandPreSelectedRace(newSettings.isAutoExpandPreSelectedRace()); - - if (newSettings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { - timer.setRefreshInterval(newSettings.getDelayBetweenAutoAdvancesInMilliseconds()); - } - for (ExpandableSortableColumn expandableSortableColumn : columnsToExpandAgain) { - expandableSortableColumn.toggleExpansion(); - } - if (newSettings.getNameOfRaceToSort() != null) { - final RaceColumn raceColumnByRaceName = getRaceColumnByRaceName(newSettings - .getNameOfRaceToSort()); - if (raceColumnByRaceName != null) { - getLeaderboardTable().sortColumn(raceColumnByRaceName, /* ascending */true); - } - } - getBusyIndicator().setBusy(oldBusyState); - } - }; - 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 */getNamesOfExpandedRaces(), - shallAddOverallDetails(), previousLeaderboard.getId(), /* fillNetPointsUncorrected */ 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; - } - - 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; - } - - @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(); - - 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()); - } - - @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.totalPoints != null) { - addedScores += entryBefore.totalPoints; - } - // else "add 0" - if (raceColumn.getName().equals(getRaceColumnName())) { - break; // we've reached the current column - stop here - } - } - return addedScores; - } - - /** - * Displays a combination of total points and maxPointsReason in bold, transparent, strike-through, depending on - * various criteria. Here's how: - * - *

    -         *                                  total 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 totalOrAddedPointsAsText = isShowAddedScores() ? addedScores - : entry.totalPoints == null ? "" : scoreFormat.format(entry.totalPoints); - String netOrAddedPointsAsText = isShowAddedScores() ? addedScores - : entry.netPoints == null ? "" : scoreFormat.format(entry.netPoints); - 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(totalOrAddedPointsAsText); - html.appendHtmlConstant(""); - } else { - html.appendHtmlConstant(" "); - html.appendHtmlConstant(netOrAddedPointsAsText); - 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_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_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, - DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL}; - } - - 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 totalPoints = object.fieldsByRaceColumnName.get(getRaceColumnName()).totalPoints; - return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); - } - - @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 CurrentSpeedOverGroundInKnots(), - 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) { - Double result = null; - LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null) { - double distanceTraveledInMeters = 0; - long timeInMilliseconds = 0; - for (LegEntryDTO legDetail : fieldsForRace.legDetails) { - if (legDetail != null) { - if (legDetail.distanceTraveledInMeters != null && legDetail.timeInMilliseconds != null) { - distanceTraveledInMeters += legDetail.distanceTraveledInMeters; - timeInMilliseconds += legDetail.timeInMilliseconds; - } else { - distanceTraveledInMeters = 0; - timeInMilliseconds = 0; - break; - } - } - } - if (timeInMilliseconds != 0) { - result = distanceTraveledInMeters / (double) timeInMilliseconds * 1000 * 3600 / 1852; - } - } - 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) { - 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.distanceTraveledInMeters != null) { - if (result == null) { - result = 0.0; - } - result += legDetail.distanceTraveledInMeters; - } - } - } - } - return result; - } - } - - 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 class CurrentSpeedOverGroundInKnots implements LegDetailField { - @Override - public Double get(LeaderboardRowDTO row) { - Double 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) { - result = lastLegDetail.currentSpeedOverGroundInKnots; - } - } - } - 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.totalPoints != 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 totals for a competitor for the entire leaderboard. - * - * @author Axel Uhl (D043530) - * - */ - private class TotalsColumn extends LeaderboardSortableColumnWithMinMax { - private final String columnStyle; - - protected TotalsColumn(String columnStyle) { - super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); - this.columnStyle = columnStyle; - setHorizontalAlignment(ALIGN_CENTER); - } - - @Override - public String getValue(LeaderboardRowDTO object) { - Double totalPoints = object.totalPoints; - return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); - } - - @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.totalsColumnTooltip()); - } - } - - 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(tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()); - 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); - if (row != null) { - redrawRow(row); - } - } - - private void redrawRow(final LeaderboardRowDTO row) { - final List leaderboardDataList = getData().getList(); - synchronized (leaderboardDataList) { - redrawRow(row, leaderboardDataList); - } - } - - /** - * Ensure that the checkbox is redrawn when the competitor selection changes - */ - @Override - public void removedFromSelection(CompetitorDTO competitor) { - final LeaderboardRowDTO row = getRow(competitor); - if (row != null) { - redrawRow(row); - } - } - - @Override - public void updateMinMax() {} - - @Override - protected ListDataProvider getListDataProvider() { - return getData(); - } - } - - 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(); - } - } - - public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - LeaderboardSettings settings, CompetitorSelectionProvider competitorSelectionProvider, - String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, - final UserAgentDetails userAgent, boolean showRaceDetails) { - this(sailingService, asyncActionsExecutor, settings, false, /* preSelectedRace */null, competitorSelectionProvider, - null, leaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails); - } - - public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, - CompetitorSelectionProvider competitorSelectionProvider, String leaderboardGroupName, - String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, - final UserAgentDetails userAgent, boolean showRaceDetails) { - this(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 */3000l), leaderboardGroupName, - leaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails, - /* competitorSearchTextBox */ null, /* showSelectionCheckbox */ true, /* optionalRaceTimesInfoProvider */ null, - /* autoExpandLastRaceColumn */ false, /* adjustTimerDelay */ true, /*autoApplyTop30Filter*/ false, false); - } - - public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, - LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, - CompetitorSelectionProvider competitorSelectionProvider, Timer timer, String leaderboardGroupName, - String leaderboardName, final ErrorReporter errorReporter, final StringMessages stringMessages, - final UserAgentDetails userAgent, boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, - boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, - boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, boolean showCompetitorFilterStatus) { - this.setTitle(stringMessages.leaderboard()); - 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.selectedLegDetails = new ArrayList(); - this.selectedRaceDetails = new ArrayList(); - this.selectedOverallDetailColumns = new ArrayList(); - this.raceTimesInfoProvider = optionalRaceTimesInfoProvider; - this.selectedManeuverDetails = new ArrayList(); - this.adjustTimerDelay = adjustTimerDelay; - this.initialCompetitorFilterHasBeenApplied = !autoApplyTopNFilter; - this.showCompetitorFilterStatus = showCompetitorFilterStatus; - overallDetailColumnMap = createOverallDetailColumnMap(); - settingsUpdatedExplicitly = !settings.isUpdateUponPlayStateChange(); - this.leaderboardUpdateListener = new ArrayList(); - if (settings.getLegDetailsToShow() != null) { - selectedLegDetails.addAll(settings.getLegDetailsToShow()); - } - if (settings.getManeuverDetailsToShow() != null) { - selectedManeuverDetails.addAll(settings.getManeuverDetailsToShow()); - } - if (settings.getRaceDetailsToShow() != null) { - selectedRaceDetails.addAll(settings.getRaceDetailsToShow()); - } - if (settings.getOverallDetailsToShow() != null) { - selectedOverallDetailColumns.addAll(settings.getOverallDetailsToShow()); - } - setAutoExpandPreSelectedRace(settings.isAutoExpandPreSelectedRace()); - this.autoExpandLastRaceColumn = autoExpandLastRaceColumn; - if (settings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { - timer.setRefreshInterval(settings.getDelayBetweenAutoAdvancesInMilliseconds()); - } - this.timer = timer; - 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(); - selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider); - RACE_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableRaceColumnHeader(); - LEG_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegColumnHeader(); - LEG_DETAIL_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumnHeader(); - RACE_COLUMN_STYLE = tableResources.cellTableStyle().cellTableRaceColumn(); - LEG_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegColumn(); - LEG_DETAIL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumn(); - TOTAL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableTotalColumn(); - leaderboardTable = new SortedCellTableWithStylableHeaders( - /* pageSize */10000, tableResources); - 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"); - getLeaderboardTable().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()); - setShowAddedScores(settings.isShowAddedScores()); - setShowCompetitorSailId(settings.isShowCompetitorSailIdColumn()); - setShowCompetitorFullName(settings.isShowCompetitorFullNameColumn()); - setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(settings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()); - if (timer.isInitialized()) { - loadCompleteLeaderboard(getLeaderboardDisplayDate()); - } - - contentPanel = new VerticalPanel(); - leaderboardTable.getElement().getStyle().setMarginTop(10, Unit.PX); - contentPanel.setStyleName(STYLE_LEADERBOARD_CONTENT); - busyIndicator = new SimpleBusyIndicator(false, 0.8f); - busyIndicator.ensureDebugId("BusyIndicator"); - - - // the information panel - if (!isEmbedded) { - Widget toolbarPanel = createToolbarPanel(); - contentPanel.add(toolbarPanel); - } - if (competitorSearchTextBox != null) { - contentPanel.add(competitorSearchTextBox); - contentPanel.add(busyIndicator); - competitorSearchTextBox.getSettingsButton().addClickHandler(new ClickHandler() { - @Override - public void onClick(ClickEvent event) { - new SettingsDialog(LeaderboardPanel.this, stringMessages).show(); - } - }); - this.competitorFilterPanel = competitorSearchTextBox; - } - SortedCellTable leaderboardTable = getLeaderboardTable(); - // leaderboardTable.getElement().getStyle().setMarginTop(5, Unit.PX); - filterControlPanel = new HorizontalPanel(); - filterControlPanel.setStyleName("LeaderboardPanel-FilterControl-Panel"); - contentPanel.add(leaderboardTable); - if (showCompetitorFilterStatus) { - contentPanel.add(createFilterDeselectionControl()); - } - setWidget(contentPanel); - raceNameForDefaultSorting = settings.getNameOfRaceToSort(); - } - - 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; - } - - 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) { - getBusyIndicator().setBusy(true); - competitorFilterPanel.clearAllActiveFilters(); - timeChanged(new Date(), null); - } - }); - 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.toggleExpansion(); - // 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); - } - } - - private void loadCompleteLeaderboard(Date date) { - if (needsDataLoading()) { - GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction(sailingService, - getLeaderboardName(), useNullAsTimePoint() ? null : date, - /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(), shallAddOverallDetails(), /* previousLeaderboard */ - getLeaderboard(), isFillNetPointsUncorrected(), timer, errorReporter, stringMessages); - this.asyncActionsExecutor.execute(getLeaderboardByNameAction, LOAD_LEADERBOARD_DATA_CATEGORY, - new AsyncCallback() { - @Override - public void onSuccess(LeaderboardDTO result) { - updateLeaderboard(result); - getBusyIndicator().setBusy(false); - } - - @Override - public void onFailure(Throwable caught) { - getBusyIndicator().setBusy(false); - getErrorReporter() - .reportError("Error trying to obtain leaderboard contents: " + caught.getMessage(), - true /* silentMode */); - } - }); - } else { - getBusyIndicator().setBusy(false); - } - } - - protected boolean isFillNetPointsUncorrected() { - return false; - } - - /** - * 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; - } - - private boolean needsDataLoading() { - return isVisible(); - } - - /** - * Determine from column expansion state which races need their leg details - */ - private Collection getNamesOfExpandedRaces() { - Collection namesOfExpandedRaces = 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()) { - namesOfExpandedRaces.add(raceColumn.getRaceColumnName()); - } - } - } - return namesOfExpandedRaces; - } - - 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. - */ - protected void updateLeaderboard(LeaderboardDTO leaderboard) { - if (leaderboard != null) { - Collection> columnsToCollapseAndExpandAgain = getExpandedRaceColumnsWhoseDisplayedLegCountChanged(leaderboard); - for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { - columnToCollapseAndExpandAgain.toggleExpansion(); - } - 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.toggleExpansion(); - } - 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.toggleExpansion(); - 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(""); - } - - List> liveRaces = leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()); - boolean hasLiveRace = !liveRaces.isEmpty(); - liveRaceLabel.setText(hasLiveRace ? getLiveRacesText() : ""); - if (hasLiveRace) { - String liveRaceText = ""; - if(liveRaces.size() == 1) { - com.sap.sse.common.Util.Pair liveRace = liveRaces.get(0); - liveRaceText = stringMessages.raceIsLive("'" + liveRace.getA().getRaceColumnName() + "'"); - } else { - String raceNames = ""; - for (com.sap.sse.common.Util.Pair liveRace : liveRaces) { - raceNames += "'" + liveRace.getA().getRaceColumnName() + "', "; - } - // remove last ", " - raceNames = raceNames.substring(0, raceNames.length() - 2); - liveRaceText = stringMessages.racesAreLive(raceNames); - } - liveRaceLabel.setText(liveRaceText); - } else { - liveRaceLabel.setText(""); - } - scoreCorrectionLastUpdateTimeLabel.setVisible(!hasLiveRace); - liveRaceLabel.setVisible(hasLiveRace); - } - informLeaderboardUpdateListenersAboutLeaderboardUpdated(leaderboard); - getBusyIndicator().setBusy(false); - } - } - - /** - * 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 - */ - private Iterable getCompetitors(RaceIdentifier race) { - Set result = new HashSet(); - 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 (iraceColumn - */ - private void correctColumnData(RaceColumn raceColumn, RaceColumnDTO race) { - int columnIndex = getRaceColumnPosition(raceColumn); - if (raceColumn.isExpansionEnabled() != race.hasTrackedRaces() || race.isMedalRace() != raceColumn.isMedalRace()) { - if (raceColumn.isExpanded()) { - raceColumn.toggleExpansion(); // 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 TotalsColumn) { - 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, getStringMessages(), - 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 TotalsColumn)) { - addColumn(new TotalsColumn(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; - } - - protected 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(getLeaderboardDisplayDate()); - } - - @Override - public void playStateChanged(PlayStates playState, PlayModes playMode) { - currentlyHandlingPlayStateChange = true; - - if(!isEmbedded) { - playPause.setHTML(getPlayPauseImgHtml(playState)); - playPause.setTitle(playState == PlayStates.Playing ? stringMessages.pauseAutomaticRefresh() : stringMessages.autoRefresh()); - } - if (!settingsUpdatedExplicitly && playMode != oldPlayMode) { - // if settings weren't explicitly modified, auto-switch to live mode settings and sort for - // any pre-selected race; we need to copy the previously selected race columns to the new RaceColumnSelection - updateSettings(LeaderboardSettingsFactory.getInstance().createNewSettingsForPlayMode( - playMode, - /* don't touch columnToSort if no race was pre-selected */ preSelectedRace == null ? null - : preSelectedRace.getRaceName(), - /* don't change nameOfRaceColumnToShow */null, - /* set nameOfRaceToShow if race was pre-selected */preSelectedRace == null ? null : preSelectedRace - .getRaceName(), getRaceColumnSelection(), /* leave showRegattaRank and overall details unchanged */ null, - /* take into account state of competitor columns*/isShowCompetitorSailId(), isShowCompetitorFullName())); - } - currentlyHandlingPlayStateChange = false; - oldPlayMode = playMode; - } - - @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(Collections.unmodifiableList(selectedManeuverDetails), - Collections.unmodifiableList(selectedLegDetails), Collections.unmodifiableList(selectedRaceDetails), - Collections.unmodifiableList(selectedOverallDetailColumns), /* All races to select */ leaderboard.getRaceList(), - raceColumnSelection.getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard), raceColumnSelection, autoExpandPreSelectedRace, - isShowAddedScores(), timer.getRefreshInterval(), isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), - isShowCompetitorSailId(), isShowCompetitorFullName(), stringMessages); - } - - @Override - public String getLocalizedShortName() { - return stringMessages.leaderboard(); - } - - private LeaderboardRowDTO getRow(CompetitorDTO competitor) { - synchronized (getData().getList()) { - for (LeaderboardRowDTO row : getData().getList()) { - if (row.competitor.equals(competitor)) { - return row; - } - } - } - return null; - } - - @Override - public void addedToSelection(CompetitorDTO competitor) { - LeaderboardRowDTO row = getRow(competitor); - if (row != null) { - leaderboardSelectionModel.setSelected(row, true); - } - } - - @Override - public void removedFromSelection(CompetitorDTO competitor) { - LeaderboardRowDTO row = getRow(competitor); - if (row != null) { - leaderboardSelectionModel.setSelected(row, false); - } - } - - @Override - public BusyIndicator getBusyIndicator() { - return busyIndicator; - } - - @Override - public boolean hasBusyIndicator() { - return true; - } - - 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); - } - } - - public void addLeaderboardUpdateListener(LeaderboardUpdateListener listener) { - this.leaderboardUpdateListener.add(listener); - } - - protected void informLeaderboardUpdateListenersAboutLeaderboardUpdated(LeaderboardDTO leaderboard) { - for (LeaderboardUpdateListener listener : 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()); - if (!liveRaces.isEmpty()) { - if(liveRaces.size() == 1) { - com.sap.sse.common.Util.Pair liveRace = liveRaces.get(0); - result = stringMessages.raceIsLive("'" + liveRace.getA().getRaceColumnName() + "'"); - } else { - String raceNames = ""; - for (com.sap.sse.common.Util.Pair liveRace : liveRaces) { - raceNames += "'" + liveRace.getA().getRaceColumnName() + "', "; - } - // remove last ", " - raceNames = raceNames.substring(0, raceNames.length() - 2); - result = stringMessages.racesAreLive(raceNames); - } - } - return result; - } - - @Override - public String getDependentCssClassName() { - return "leaderboard"; - } -} +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.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.VerticalPanel; +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.InvertibleComparator; +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.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.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.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.leaderboard.LeaderboardSettings.RaceColumnSelectionStrategies; +import com.sap.sailing.gwt.ui.shared.RaceTimesInfoDTO; +import com.sap.sse.common.Util; +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.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.controls.busyindicator.BusyIndicator; +import com.sap.sse.gwt.client.controls.busyindicator.SimpleBusyIndicator; +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.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.useragent.UserAgentDetails; + +/** + * A leaderboard essentially consists of a table widget that in its columns displays the entries. + * + * @author Axel Uhl (D043530) + * + */ +public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayStateListener, DisplayedLeaderboardRowsProvider, + Component, IsEmbeddableComponent, CompetitorSelectionChangeListener, LeaderboardFetcher { + 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"; + + interface RaceColumnTemplates extends SafeHtmlTemplates { + @SafeHtmlTemplates.Template("

    ") + SafeHtml cellFrameWithTextColorAndFleetBorder(String textColor, String borderStyle); + + @SafeHtmlTemplates.Template("
    ") + SafeHtml cellFrameWithTextColor(String textColor); + + @SafeHtmlTemplates.Template("
    ") + SafeHtml cellWithImageResourceAndText(ImageResource image, String text); + } + + private static RaceColumnTemplates raceColumnTemplate = GWT.create(RaceColumnTemplates.class); + + /** + * 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 String leaderboardName; + + private final ErrorReporter errorReporter; + + private final StringMessages stringMessages; + + private final SortedCellTable 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; + + /** + * 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; + + /** + * 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; + + private final List selectedOverallDetailColumns; + + private final Map> overallDetailColumnMap; + + private RaceColumnSelection raceColumnSelection; + + protected final String RACE_COLUMN_HEADER_STYLE; + + protected final String LEG_DETAIL_COLUMN_HEADER_STYLE; + + protected final String LEG_DETAIL_COLUMN_STYLE; + + protected final String LEG_COLUMN_HEADER_STYLE; + + protected final String RACE_COLUMN_STYLE; + + protected final String LEG_COLUMN_STYLE; + + protected final String TOTAL_COLUMN_STYLE; + + 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 VerticalPanel contentPanel; + + private HorizontalPanel refreshAndSettingsPanel; + private Label scoreCorrectionLastUpdateTimeLabel; + private Label scoreCorrectionCommentLabel; + private Label liveRaceLabel; + + private boolean isEmbedded; + + private static final LeaderboardResources resources = GWT.create(LeaderboardResources.class); + private static final ComponentResources componentResources = GWT.create(ComponentResources.class); + private static final LeaderboardTableResources tableResources = GWT.create(LeaderboardTableResources.class); + + private ImageResource pauseIcon; + private ImageResource playIcon; + + private final BusyIndicator busyIndicator; + + /** + * Tells whether the leaderboard settings were explicitly changed by an external call to + * {@link #updateSettings(LeaderboardSettings)}. If so, a {@link #playStateChanged(PlayStates, PlayModes) play state + * change} will not automatically lead to a settings change. + */ + private boolean settingsUpdatedExplicitly = false; + + /** + * Tells if the leaderboard is currently handling a {@link #playStateChanged(PlayStates, PlayModes) play state + * change}. If this is the case, a call to {@link #updateSettings(LeaderboardSettings)} won't set the + * {@link #settingsUpdatedExplicitly} flag. + */ + private boolean currentlyHandlingPlayStateChange; + + private PlayModes oldPlayMode; + + 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 List leaderboardUpdateListener; + + private boolean initialCompetitorFilterHasBeenApplied = false; + private final boolean showCompetitorFilterStatus; + + private CompetitorFilterPanel competitorFilterPanel; + + protected StringMessages getStringMessages() { + return stringMessages; + } + + @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 VerticalPanel getContentPanel() { + return contentPanel; + } + + protected ImageResource getSettingsIcon() { + return componentResources.settingsIcon(); + } + + public void updateSettings(final LeaderboardSettings newSettings) { + boolean oldShallAddOverallDetails = shallAddOverallDetails(); + if (newSettings.getOverallDetailsToShow() != null) { + selectedOverallDetailColumns.clear(); + selectedOverallDetailColumns.addAll(newSettings.getOverallDetailsToShow()); + } + if (!newSettings.isUpdateUponPlayStateChange() || !currentlyHandlingPlayStateChange) { + settingsUpdatedExplicitly = true; + } + 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.toggleExpansion(); + 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()); + } + // 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; + } + + final boolean oldBusyState = getBusyIndicator().isBusy(); + getBusyIndicator().setBusy(true); + Runnable doWhenNecessaryDetailHasBeenLoaded = new Runnable() { + @Override + public void run() { + setAutoExpandPreSelectedRace(false); // avoid expansion during updateLeaderboard(...); will expand later + // if it was expanded before + // update leaderboard after settings panel column selection change + updateLeaderboard(leaderboard); + setAutoExpandPreSelectedRace(newSettings.isAutoExpandPreSelectedRace()); + + if (newSettings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { + timer.setRefreshInterval(newSettings.getDelayBetweenAutoAdvancesInMilliseconds()); + } + for (ExpandableSortableColumn expandableSortableColumn : columnsToExpandAgain) { + expandableSortableColumn.toggleExpansion(); + } + if (newSettings.getNameOfRaceToSort() != null) { + final RaceColumn raceColumnByRaceName = getRaceColumnByRaceName(newSettings + .getNameOfRaceToSort()); + if (raceColumnByRaceName != null) { + getLeaderboardTable().sortColumn(raceColumnByRaceName, /* ascending */true); + } + } + getBusyIndicator().setBusy(oldBusyState); + } + }; + 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 */getNamesOfExpandedRaces(), + shallAddOverallDetails(), previousLeaderboard.getId(), /* fillNetPointsUncorrected */ 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; + } + + 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; + } + + @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(); + + 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()); + } + + @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.totalPoints != null) { + addedScores += entryBefore.totalPoints; + } + // else "add 0" + if (raceColumn.getName().equals(getRaceColumnName())) { + break; // we've reached the current column - stop here + } + } + return addedScores; + } + + /** + * Displays a combination of total points and maxPointsReason in bold, transparent, strike-through, depending on + * various criteria. Here's how: + * + *

    +         *                                  total 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 totalOrAddedPointsAsText = isShowAddedScores() ? addedScores + : entry.totalPoints == null ? "" : scoreFormat.format(entry.totalPoints); + String netOrAddedPointsAsText = isShowAddedScores() ? addedScores + : entry.netPoints == null ? "" : scoreFormat.format(entry.netPoints); + 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(totalOrAddedPointsAsText); + html.appendHtmlConstant(""); + } else { + html.appendHtmlConstant(" "); + html.appendHtmlConstant(netOrAddedPointsAsText); + 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_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_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, + DetailType.RACE_RATIO_BETWEEN_TIME_SINCE_LAST_POSITION_FIX_AND_AVERAGE_SAMPLING_INTERVAL}; + } + + 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 totalPoints = object.fieldsByRaceColumnName.get(getRaceColumnName()).totalPoints; + return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); + } + + @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 CurrentSpeedOverGroundInKnots(), + 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) { + Double result = null; + LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); + if (fieldsForRace != null && fieldsForRace.legDetails != null) { + double distanceTraveledInMeters = 0; + long timeInMilliseconds = 0; + for (LegEntryDTO legDetail : fieldsForRace.legDetails) { + if (legDetail != null) { + if (legDetail.distanceTraveledInMeters != null && legDetail.timeInMilliseconds != null) { + distanceTraveledInMeters += legDetail.distanceTraveledInMeters; + timeInMilliseconds += legDetail.timeInMilliseconds; + } else { + distanceTraveledInMeters = 0; + timeInMilliseconds = 0; + break; + } + } + } + if (timeInMilliseconds != 0) { + result = distanceTraveledInMeters / (double) timeInMilliseconds * 1000 * 3600 / 1852; + } + } + 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) { + 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.distanceTraveledInMeters != null) { + if (result == null) { + result = 0.0; + } + result += legDetail.distanceTraveledInMeters; + } + } + } + } + return result; + } + } + + 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 class CurrentSpeedOverGroundInKnots implements LegDetailField { + @Override + public Double get(LeaderboardRowDTO row) { + Double 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) { + result = lastLegDetail.currentSpeedOverGroundInKnots; + } + } + } + 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.totalPoints != 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 totals for a competitor for the entire leaderboard. + * + * @author Axel Uhl (D043530) + * + */ + private class TotalsColumn extends LeaderboardSortableColumnWithMinMax { + private final String columnStyle; + + protected TotalsColumn(String columnStyle) { + super(new TextCell(), SortingOrder.ASCENDING, LeaderboardPanel.this); + this.columnStyle = columnStyle; + setHorizontalAlignment(ALIGN_CENTER); + } + + @Override + public String getValue(LeaderboardRowDTO object) { + Double totalPoints = object.totalPoints; + return "" + (totalPoints == null ? "" : scoreFormat.format(totalPoints)); + } + + @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.totalsColumnTooltip()); + } + } + + 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(tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell()); + 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); + if (row != null) { + redrawRow(row); + } + } + + private void redrawRow(final LeaderboardRowDTO row) { + final List leaderboardDataList = getData().getList(); + synchronized (leaderboardDataList) { + redrawRow(row, leaderboardDataList); + } + } + + /** + * Ensure that the checkbox is redrawn when the competitor selection changes + */ + @Override + public void removedFromSelection(CompetitorDTO competitor) { + final LeaderboardRowDTO row = getRow(competitor); + if (row != null) { + redrawRow(row); + } + } + + @Override + public void updateMinMax() {} + + @Override + protected ListDataProvider getListDataProvider() { + return getData(); + } + } + + 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(); + } + } + + public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + LeaderboardSettings settings, CompetitorSelectionProvider competitorSelectionProvider, + String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, + final UserAgentDetails userAgent, boolean showRaceDetails) { + this(sailingService, asyncActionsExecutor, settings, false, /* preSelectedRace */null, competitorSelectionProvider, + null, leaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails); + } + + public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, + CompetitorSelectionProvider competitorSelectionProvider, String leaderboardGroupName, + String leaderboardName, ErrorReporter errorReporter, final StringMessages stringMessages, + final UserAgentDetails userAgent, boolean showRaceDetails) { + this(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 */3000l), leaderboardGroupName, + leaderboardName, errorReporter, stringMessages, userAgent, showRaceDetails, + /* competitorSearchTextBox */ null, /* showSelectionCheckbox */ true, /* optionalRaceTimesInfoProvider */ null, + /* autoExpandLastRaceColumn */ false, /* adjustTimerDelay */ true, /*autoApplyTop30Filter*/ false, false); + } + + public LeaderboardPanel(SailingServiceAsync sailingService, AsyncActionsExecutor asyncActionsExecutor, + LeaderboardSettings settings, boolean isEmbedded, RegattaAndRaceIdentifier preSelectedRace, + CompetitorSelectionProvider competitorSelectionProvider, Timer timer, String leaderboardGroupName, + String leaderboardName, final ErrorReporter errorReporter, final StringMessages stringMessages, + final UserAgentDetails userAgent, boolean showRaceDetails, CompetitorFilterPanel competitorSearchTextBox, + boolean showSelectionCheckbox, RaceTimesInfoProvider optionalRaceTimesInfoProvider, + boolean autoExpandLastRaceColumn, boolean adjustTimerDelay, boolean autoApplyTopNFilter, boolean showCompetitorFilterStatus) { + this.setTitle(stringMessages.leaderboard()); + 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.selectedLegDetails = new ArrayList(); + this.selectedRaceDetails = new ArrayList(); + this.selectedOverallDetailColumns = new ArrayList(); + this.raceTimesInfoProvider = optionalRaceTimesInfoProvider; + this.selectedManeuverDetails = new ArrayList(); + this.adjustTimerDelay = adjustTimerDelay; + this.initialCompetitorFilterHasBeenApplied = !autoApplyTopNFilter; + this.showCompetitorFilterStatus = showCompetitorFilterStatus; + overallDetailColumnMap = createOverallDetailColumnMap(); + settingsUpdatedExplicitly = !settings.isUpdateUponPlayStateChange(); + this.leaderboardUpdateListener = new ArrayList(); + if (settings.getLegDetailsToShow() != null) { + selectedLegDetails.addAll(settings.getLegDetailsToShow()); + } + if (settings.getManeuverDetailsToShow() != null) { + selectedManeuverDetails.addAll(settings.getManeuverDetailsToShow()); + } + if (settings.getRaceDetailsToShow() != null) { + selectedRaceDetails.addAll(settings.getRaceDetailsToShow()); + } + if (settings.getOverallDetailsToShow() != null) { + selectedOverallDetailColumns.addAll(settings.getOverallDetailsToShow()); + } + setAutoExpandPreSelectedRace(settings.isAutoExpandPreSelectedRace()); + this.autoExpandLastRaceColumn = autoExpandLastRaceColumn; + if (settings.getDelayBetweenAutoAdvancesInMilliseconds() != null) { + timer.setRefreshInterval(settings.getDelayBetweenAutoAdvancesInMilliseconds()); + } + this.timer = timer; + 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(); + selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider); + RACE_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableRaceColumnHeader(); + LEG_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegColumnHeader(); + LEG_DETAIL_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumnHeader(); + RACE_COLUMN_STYLE = tableResources.cellTableStyle().cellTableRaceColumn(); + LEG_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegColumn(); + LEG_DETAIL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumn(); + TOTAL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableTotalColumn(); + leaderboardTable = new SortedCellTableWithStylableHeaders( + /* pageSize */10000, tableResources); + 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"); + getLeaderboardTable().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()); + setShowAddedScores(settings.isShowAddedScores()); + setShowCompetitorSailId(settings.isShowCompetitorSailIdColumn()); + setShowCompetitorFullName(settings.isShowCompetitorFullNameColumn()); + setShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(settings.isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor()); + if (timer.isInitialized()) { + loadCompleteLeaderboard(getLeaderboardDisplayDate()); + } + + contentPanel = new VerticalPanel(); + leaderboardTable.getElement().getStyle().setMarginTop(10, Unit.PX); + contentPanel.setStyleName(STYLE_LEADERBOARD_CONTENT); + busyIndicator = new SimpleBusyIndicator(false, 0.8f); + busyIndicator.ensureDebugId("BusyIndicator"); + + + // the information panel + if (!isEmbedded) { + Widget toolbarPanel = createToolbarPanel(); + contentPanel.add(toolbarPanel); + } + if (competitorSearchTextBox != null) { + contentPanel.add(competitorSearchTextBox); + contentPanel.add(busyIndicator); + competitorSearchTextBox.getSettingsButton().addClickHandler(new ClickHandler() { + @Override + public void onClick(ClickEvent event) { + new SettingsDialog(LeaderboardPanel.this, stringMessages).show(); + } + }); + this.competitorFilterPanel = competitorSearchTextBox; + } + SortedCellTable leaderboardTable = getLeaderboardTable(); + // leaderboardTable.getElement().getStyle().setMarginTop(5, Unit.PX); + filterControlPanel = new HorizontalPanel(); + filterControlPanel.setStyleName("LeaderboardPanel-FilterControl-Panel"); + contentPanel.add(leaderboardTable); + if (showCompetitorFilterStatus) { + contentPanel.add(createFilterDeselectionControl()); + } + setWidget(contentPanel); + raceNameForDefaultSorting = settings.getNameOfRaceToSort(); + } + + 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; + } + + 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) { + getBusyIndicator().setBusy(true); + competitorFilterPanel.clearAllActiveFilters(); + timeChanged(new Date(), null); + } + }); + 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.toggleExpansion(); + // 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); + } + } + + private void loadCompleteLeaderboard(Date date) { + if (needsDataLoading()) { + GetLeaderboardByNameAction getLeaderboardByNameAction = new GetLeaderboardByNameAction(sailingService, + getLeaderboardName(), useNullAsTimePoint() ? null : date, + /* namesOfRacesForWhichToLoadLegDetails */getNamesOfExpandedRaces(), shallAddOverallDetails(), /* previousLeaderboard */ + getLeaderboard(), isFillNetPointsUncorrected(), timer, errorReporter, stringMessages); + this.asyncActionsExecutor.execute(getLeaderboardByNameAction, LOAD_LEADERBOARD_DATA_CATEGORY, + new AsyncCallback() { + @Override + public void onSuccess(LeaderboardDTO result) { + updateLeaderboard(result); + getBusyIndicator().setBusy(false); + } + + @Override + public void onFailure(Throwable caught) { + getBusyIndicator().setBusy(false); + getErrorReporter() + .reportError("Error trying to obtain leaderboard contents: " + caught.getMessage(), + true /* silentMode */); + } + }); + } else { + getBusyIndicator().setBusy(false); + } + } + + protected boolean isFillNetPointsUncorrected() { + return false; + } + + /** + * 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; + } + + private boolean needsDataLoading() { + return isVisible(); + } + + /** + * Determine from column expansion state which races need their leg details + */ + private Collection getNamesOfExpandedRaces() { + Collection namesOfExpandedRaces = 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()) { + namesOfExpandedRaces.add(raceColumn.getRaceColumnName()); + } + } + } + return namesOfExpandedRaces; + } + + 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. + */ + protected void updateLeaderboard(LeaderboardDTO leaderboard) { + if (leaderboard != null) { + Collection> columnsToCollapseAndExpandAgain = getExpandedRaceColumnsWhoseDisplayedLegCountChanged(leaderboard); + for (RaceColumn columnToCollapseAndExpandAgain : columnsToCollapseAndExpandAgain) { + columnToCollapseAndExpandAgain.toggleExpansion(); + } + 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.toggleExpansion(); + } + 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.toggleExpansion(); + 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); + getBusyIndicator().setBusy(false); + } + } + + /** + * 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 + */ + private Iterable getCompetitors(RaceIdentifier race) { + Set result = new HashSet(); + 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 (iraceColumn + */ + private void correctColumnData(RaceColumn raceColumn, RaceColumnDTO race) { + int columnIndex = getRaceColumnPosition(raceColumn); + if (raceColumn.isExpansionEnabled() != race.hasTrackedRaces() || race.isMedalRace() != raceColumn.isMedalRace()) { + if (raceColumn.isExpanded()) { + raceColumn.toggleExpansion(); // 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 TotalsColumn) { + 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, getStringMessages(), + 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 TotalsColumn)) { + addColumn(new TotalsColumn(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; + } + + protected 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(getLeaderboardDisplayDate()); + } + + @Override + public void playStateChanged(PlayStates playState, PlayModes playMode) { + currentlyHandlingPlayStateChange = true; + + if(!isEmbedded) { + playPause.setHTML(getPlayPauseImgHtml(playState)); + playPause.setTitle(playState == PlayStates.Playing ? stringMessages.pauseAutomaticRefresh() : stringMessages.autoRefresh()); + } + if (!settingsUpdatedExplicitly && playMode != oldPlayMode) { + // if settings weren't explicitly modified, auto-switch to live mode settings and sort for + // any pre-selected race; we need to copy the previously selected race columns to the new RaceColumnSelection + updateSettings(LeaderboardSettingsFactory.getInstance().createNewSettingsForPlayMode( + playMode, + /* don't touch columnToSort if no race was pre-selected */ preSelectedRace == null ? null + : preSelectedRace.getRaceName(), + /* don't change nameOfRaceColumnToShow */null, + /* set nameOfRaceToShow if race was pre-selected */preSelectedRace == null ? null : preSelectedRace + .getRaceName(), getRaceColumnSelection(), /* leave showRegattaRank and overall details unchanged */ null, + /* take into account state of competitor columns*/isShowCompetitorSailId(), isShowCompetitorFullName())); + } + currentlyHandlingPlayStateChange = false; + oldPlayMode = playMode; + } + + @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(Collections.unmodifiableList(selectedManeuverDetails), + Collections.unmodifiableList(selectedLegDetails), Collections.unmodifiableList(selectedRaceDetails), + Collections.unmodifiableList(selectedOverallDetailColumns), /* All races to select */ leaderboard.getRaceList(), + raceColumnSelection.getSelectedRaceColumnsOrderedAsInLeaderboard(leaderboard), raceColumnSelection, autoExpandPreSelectedRace, + isShowAddedScores(), timer.getRefreshInterval(), isShowOverallColumnWithNumberOfRacesCompletedPerCompetitor(), + isShowCompetitorSailId(), isShowCompetitorFullName(), stringMessages); + } + + @Override + public String getLocalizedShortName() { + return stringMessages.leaderboard(); + } + + private LeaderboardRowDTO getRow(CompetitorDTO competitor) { + synchronized (getData().getList()) { + for (LeaderboardRowDTO row : getData().getList()) { + if (row.competitor.equals(competitor)) { + return row; + } + } + } + return null; + } + + @Override + public void addedToSelection(CompetitorDTO competitor) { + LeaderboardRowDTO row = getRow(competitor); + if (row != null) { + leaderboardSelectionModel.setSelected(row, true); + } + } + + @Override + public void removedFromSelection(CompetitorDTO competitor) { + LeaderboardRowDTO row = getRow(competitor); + if (row != null) { + leaderboardSelectionModel.setSelected(row, false); + } + } + + @Override + public BusyIndicator getBusyIndicator() { + return busyIndicator; + } + + @Override + public boolean hasBusyIndicator() { + return true; + } + + 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); + } + } + + public void addLeaderboardUpdateListener(LeaderboardUpdateListener listener) { + this.leaderboardUpdateListener.add(listener); + } + + protected void informLeaderboardUpdateListenersAboutLeaderboardUpdated(LeaderboardDTO leaderboard) { + for (LeaderboardUpdateListener listener : 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) { + com.sap.sse.common.Util.Pair liveRace = liveRaces.get(0); + String text = "'" + liveRace.getA().getRaceColumnName() + "'"; + result = isMeta ? stringMessages.regattaIsLive(text) : stringMessages.raceIsLive(text); + } else { + String names = ""; + for (com.sap.sse.common.Util.Pair liveRace : liveRaces) { + names += "'" + liveRace.getA().getRaceColumnName() + "', "; + } + // remove last ", " + names = names.substring(0, names.length() - 2); + result = isMeta ? stringMessages.regattasAreLive(names) : stringMessages.racesAreLive(names); + } + } + return result; + } + + @Override + public String getDependentCssClassName() { + return "leaderboard"; + } +} From 707e7bfbbdb7ef1122cba4d919c39468aa9776e9 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Thu, 3 Dec 2015 13:08:11 +0100 Subject: [PATCH 094/306] performance optimization in RefreshableSingleSelectionModel --- .../gwt/client/celltable/RefreshableSingleSelectionModel.java | 1 + 1 file changed, 1 insertion(+) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index d5d135358b6..262a40c6c8e 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -63,6 +63,7 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel boolean isEqual = comp == null ? selected.equals(it) : comp.representSameEntity(selected, it); if (isEqual) { setSelected(it, true); + break; } } } From cd77f71f88121f2d62dbfd6a8ebbc9bb707fcf54 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Thu, 3 Dec 2015 14:19:47 +0100 Subject: [PATCH 095/306] newSelectionModel: made RefreshableSelectionModels a Listener of ListDataProvider --- .../RefreshableMultiSelectionModel.java | 117 ++++++++++++++++- .../RefreshableSingleSelectionModel.java | 123 +++++++++++++++--- 2 files changed, 217 insertions(+), 23 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 00bbec48ca3..61dc25de829 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -1,33 +1,62 @@ package com.sap.sse.gwt.client.celltable; +import java.util.List; import java.util.Set; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.cellview.client.CellTable; +import com.google.gwt.view.client.HasData; +import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.MultiSelectionModel; import com.google.gwt.view.client.ProvidesKey; +import com.google.gwt.view.client.Range; +import com.google.gwt.view.client.RangeChangeEvent.Handler; import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionModel; /** - * TODO Lukas: Add Javadoc - * + * This {@link RefreshableMultiSelectionModel} have the property that it is refreshable. That means if the + * {@link CellTable} have new Elements you can call + * {@link RefreshableMultiSelectionModel#refreshSelectionModel(Iterable) refreshSelectionModel(Iterable)} and the + * elements with {@link EntityIdentityComparator#representSameEntity(Object, Object) the same identity} will be reselected. + * * @param + * the type of {@link CellTable} entries */ -public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel { +public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; + private final ListDataProvider provider; /** - * TODO Lukas: Add Javadoc + * @param comp + * {@link EntityIdentityComparator} to compare the identity of the objects + * @param provider + * {@link ListDataProvider} to register this as listener and get the entries */ - public RefreshableMultiSelectionModel(EntityIdentityComparator comp) { + public RefreshableMultiSelectionModel(EntityIdentityComparator comp, ListDataProvider provider) { super(); this.comp = comp; + this.provider = provider; + if (this.provider != null) { + this.provider.addDataDisplay(this); + } } /** - * TODO Lukas: Add Javadoc + * @param keyProvider + * {@link ProvidesKey} for the super class constructor + * @param comp + * {@link EntityIdentityComparator} to compare the identity of the objects + * @param provider + * {@link ListDataProvider} to register this as listener and get the entries */ - public RefreshableMultiSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { + public RefreshableMultiSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp, ListDataProvider provider) { super(keyProvider); this.comp = comp; + this.provider = provider; + if (this.provider != null) { + this.provider.addDataDisplay(this); + } } @Override @@ -84,4 +113,78 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im SelectionChangeEvent.fire(this); } } + + @Override + public void setRowData(int start, List values) { + refreshSelectionModel(provider.getList()); + } + + @Override + public int getRowCount() { + return Integer.MAX_VALUE; + } + + @Override + public Range getVisibleRange() { + return new Range(0, Integer.MAX_VALUE); + } + + @Override + public int getVisibleItemCount() { + return Integer.MAX_VALUE; + } + + @Override + public HandlerRegistration addRangeChangeHandler(Handler handler) { + return null; + } + @Override + public HandlerRegistration addRowCountChangeHandler( + com.google.gwt.view.client.RowCountChangeEvent.Handler handler) { + return null; + } + @Override + public boolean isRowCountExact() { + return false; + } + @Override + public void setRowCount(int count) { + return; + } + @Override + public void setRowCount(int count, boolean isExact) { + return; + } + @Override + public void setVisibleRange(int start, int length) { + return; + } + @Override + public void setVisibleRange(Range range) { + return; + } + @Override + public HandlerRegistration addCellPreviewHandler(com.google.gwt.view.client.CellPreviewEvent.Handler handler) { + return null; + } + @Override + public SelectionModel getSelectionModel() { + return null; + } + @Override + public T getVisibleItem(int indexOnPage) { + return null; + } + @Override + public Iterable getVisibleItems() { + return null; + } + @Override + public void setSelectionModel(SelectionModel selectionModel) { + return; + } + @Override + public void setVisibleRangeAndClearData(Range range, boolean forceRangeChangeEvent) { + clear(); + } } \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index 262a40c6c8e..44a9c9e6bed 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -1,30 +1,55 @@ package com.sap.sse.gwt.client.celltable; -import java.util.Set; +import java.util.List; + +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.view.client.HasData; +import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.ProvidesKey; +import com.google.gwt.view.client.Range; import com.google.gwt.view.client.SelectionChangeEvent; +import com.google.gwt.view.client.SelectionModel; import com.google.gwt.view.client.SingleSelectionModel; +import com.google.gwt.view.client.RangeChangeEvent.Handler; /** * TODO Lukas: Add Javadoc */ -public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel { +public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; + private final ListDataProvider provider; /** - * TODO Lukas: Add Javadoc + * @param comp + * {@link EntityIdentityComparator} to compare the identity of the objects + * @param provider + * {@link ListDataProvider} to register this as listener and get the entries */ - public RefreshableSingleSelectionModel(EntityIdentityComparator comp) { + public RefreshableSingleSelectionModel(EntityIdentityComparator comp, ListDataProvider provider) { super(); this.comp=comp; + this.provider = provider; + if (this.provider != null) { + this.provider.addDataDisplay(this); + } } + /** - * TODO Lukas: Add Javadoc + * @param keyProvider + * {@link ProvidesKey} for the super class constructor + * @param comp + * {@link EntityIdentityComparator} to compare the identity of the objects + * @param provider + * {@link ListDataProvider} to register this as listener and get the entries */ - public RefreshableSingleSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { + public RefreshableSingleSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp, ListDataProvider provider) { super(keyProvider); this.comp =comp; + this.provider = provider; + if (this.provider != null) { + this.provider.addDataDisplay(this); + } } @Override @@ -37,16 +62,8 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel if (comp == null) { super.setSelected(item, selected); } else { - T wasSelectedBefore = null; - Set selectedSet = getSelectedSet(); - for (T it : selectedSet) { - if(comp.representSameEntity(it, item)) { - wasSelectedBefore = it; - break; - } - } - if(wasSelectedBefore == null) { - super.setSelected(wasSelectedBefore, false); //This old version of item will be deleted with the next clear() + if(comp.representSameEntity(getSelectedObject(), item)) { + super.setSelected(getSelectedObject(), false); //This old version of item will be deleted with the next clear() super.setSelected(item, selected); } else { super.setSelected(item, selected); @@ -69,4 +86,78 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel } SelectionChangeEvent.fire(this); } + + @Override + public void setRowData(int start, List values) { + refreshSelectionModel(provider.getList()); + } + + @Override + public int getRowCount() { + return Integer.MAX_VALUE; + } + + @Override + public Range getVisibleRange() { + return new Range(0, Integer.MAX_VALUE); + } + + @Override + public int getVisibleItemCount() { + return Integer.MAX_VALUE; + } + + @Override + public HandlerRegistration addRangeChangeHandler(Handler handler) { + return null; + } + @Override + public HandlerRegistration addRowCountChangeHandler( + com.google.gwt.view.client.RowCountChangeEvent.Handler handler) { + return null; + } + @Override + public boolean isRowCountExact() { + return false; + } + @Override + public void setRowCount(int count) { + return; + } + @Override + public void setRowCount(int count, boolean isExact) { + return; + } + @Override + public void setVisibleRange(int start, int length) { + return; + } + @Override + public void setVisibleRange(Range range) { + return; + } + @Override + public HandlerRegistration addCellPreviewHandler(com.google.gwt.view.client.CellPreviewEvent.Handler handler) { + return null; + } + @Override + public SelectionModel getSelectionModel() { + return null; + } + @Override + public T getVisibleItem(int indexOnPage) { + return null; + } + @Override + public Iterable getVisibleItems() { + return null; + } + @Override + public void setSelectionModel(SelectionModel selectionModel) { + return; + } + @Override + public void setVisibleRangeAndClearData(Range range, boolean forceRangeChangeEvent) { + clear(); + } } \ No newline at end of file From 3fc535963e81c30e316ace9446ccbf29d97c018b Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 3 Dec 2015 17:04:19 +0100 Subject: [PATCH 096/306] added a few TODOs/FIXMEs for Jonas --- .../shared/racemap/AdvantageLineAnimator.java | 16 +++++++--------- .../gwt/ui/client/shared/racemap/RaceMap.java | 8 ++++---- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java index 3bcb5c0ee77..01c33e9bbeb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java @@ -12,7 +12,7 @@ import com.sap.sailing.gwt.ui.shared.GPSFixDTO; class AdvantageLineAnimator extends Timer { private static final double MILLIS_TO_HOURS_FACTOR = 3600000; - private Polyline advantageLine; + private final Polyline advantageLine; private GPSFixDTO lastBoatFix; private long lastTime; private int repetitions = -1; @@ -26,7 +26,7 @@ class AdvantageLineAnimator extends Timer { scheduleRepeating(100, 10); } - public void scheduleRepeating(int periodMillis, int repetitions) { + public void scheduleRepeating(int periodMillis, int repetitions) { // TODO Jonas: use duration instead of repetitions and check time as we go this.lastTime = System.currentTimeMillis(); this.scheduleRepeating(periodMillis); this.repetitions = repetitions; @@ -36,29 +36,27 @@ class AdvantageLineAnimator extends Timer { public void run() { if (repetitions > 0) { repetitions--; - } else if (repetitions == 0) { + } else if (repetitions == 0) { // TODO Jonas: see above, use duration / target time this.cancel(); return; } - - if(advantageLine != null) { + if (advantageLine != null) { long time = System.currentTimeMillis(); long deltaTime = time - lastTime; lastTime = time; - if (lastBoatFix != null) { Position oldPosition = lastBoatFix.position; - Position newPosition = oldPosition.translateRhumb( + Position newPosition = oldPosition.translateRhumb( // TODO Jonas: use great circle navigation new DegreeBearingImpl(lastBoatFix.speedWithBearing.bearingInDegrees), new NauticalMileDistance(deltaTime / MILLIS_TO_HOURS_FACTOR * lastBoatFix.speedWithBearing.speedInKnots)); - final double latDelta = newPosition.getLatDeg() - oldPosition.getLatDeg(); + final double latDelta = newPosition.getLatDeg() - oldPosition.getLatDeg(); // FIXME Jonas: this would be a delta in "logical coordinates" but not Google Map coordinates final double lngDelta = newPosition.getLngDeg() - oldPosition.getLngDeg(); final MVCArray path = this.advantageLine.getPath(); LatLng pos1 = path.pop(); LatLng pos2 = path.pop(); - path.push(LatLng.newInstance(pos2.getLatitude() + latDelta, pos2.getLongitude() + lngDelta)); + path.push(LatLng.newInstance(pos2.getLatitude() + latDelta, pos2.getLongitude() + lngDelta)); // FIXME Jonas: must not use Position's lat/lng to draw on map; always use coordinateSystem to map path.push(LatLng.newInstance(pos1.getLatitude() + latDelta, pos1.getLongitude() + lngDelta)); } } 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 f8a3d3ae8fc..97a78531dc7 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 @@ -804,7 +804,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe List> windSourcesToShow = new ArrayList>(); if (windInfo != null) { lastCombinedWindTrackInfoDTO = windInfo; - showAdvantageLine(competitorsToShow, newTime); +// showAdvantageLine(competitorsToShow, newTime); // TODO bug 1662: can we live without this call and assume that updating the advantage line in case the boats are moved is enough? for (WindSource windSource : windInfo.windTrackInfoByWindSource.keySet()) { WindTrackInfoDTO windTrackInfoDTO = windInfo.windTrackInfoByWindSource.get(windSource); switch (windSource.getType()) { @@ -896,7 +896,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe startedProcessingRequestID = requestID; // Do boat specific actions Map> boatData = raceMapDataDTO.boatPositions; - long timeForPositionTransitionMillis = calculateTimeForPositionTransition(newTime, oldTime); + long timeForPositionTransitionMillis = calculateTimeForPositionTransitionInMillis(newTime, oldTime); fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, timeForPositionTransitionMillis); showBoatsOnMap(newTime, timeForPositionTransitionMillis, getCompetitorsToShow()); showCompetitorInfoOnMap(newTime, timeForPositionTransitionMillis, competitorSelection.getSelectedFilteredCompetitors()); @@ -917,7 +917,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); // 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); + showAdvantageLine(competitorsToShow, newTime); // Rezoom the map LatLngBounds zoomToBounds = null; @@ -1136,7 +1136,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe } } - private long calculateTimeForPositionTransition(final Date newTime, final Date oldTime) { + 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) { From 8800b2213099bebe16356dc4d89f185e76efe9ee Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 3 Dec 2015 17:10:12 +0100 Subject: [PATCH 097/306] performance, bug 3380: replacing TrackedRaceImpl.windSourcesToExclude by a ConcurrentHashMap, removing the need to use "synchronized" --- .../domain/tracking/impl/TrackedRaceImpl.java | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index f5d9a7f3b1f..b73c003de3e 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -183,7 +183,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl * By default, all wind sources are used, none are excluded. However, e.g., for performance reasons, particular wind * sources such as the track-based estimation wind source, may be excluded by adding them to this set. */ - private final Set windSourcesToExclude; + private final ConcurrentHashMap windSourcesToExclude; /** * Keeps the oldest timestamp that is fed into this tracked race, either from a boat fix, a mark fix, a race @@ -424,7 +424,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl + race.getName(), /* fair */ false); this.cacheInvalidationTimerLock = new Object(); this.updateCount = 0; - this.windSourcesToExclude = new HashSet(); + this.windSourcesToExclude = new ConcurrentHashMap<>(); this.directionFromStartToNextMarkCache = new HashMap>(); this.millisecondsOverWhichToAverageSpeed = millisecondsOverWhichToAverageSpeed; this.delayToLiveInMillis = delayToLiveInMillis; @@ -1679,24 +1679,20 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl @Override public Set getWindSourcesToExclude() { - synchronized (windSourcesToExclude) { - return Collections.unmodifiableSet(windSourcesToExclude); - } + return Collections.unmodifiableSet(windSourcesToExclude.keySet()); } @Override public void setWindSourcesToExclude(Iterable windSourcesToExclude) { - Set old = new HashSet<>(this.windSourcesToExclude); - synchronized (this.windSourcesToExclude) { - LockUtil.lockForRead(getSerializationLock()); - try { - this.windSourcesToExclude.clear(); - for (WindSource windSourceToExclude : windSourcesToExclude) { - this.windSourcesToExclude.add(windSourceToExclude); - } - } finally { - LockUtil.unlockAfterRead(getSerializationLock()); + Set old = new HashSet<>(getWindSourcesToExclude()); + LockUtil.lockForRead(getSerializationLock()); + try { + this.windSourcesToExclude.clear(); + for (WindSource windSourceToExclude : windSourcesToExclude) { + this.windSourcesToExclude.put(windSourceToExclude, this); } + } finally { + LockUtil.unlockAfterRead(getSerializationLock()); } if (!old.equals(this.windSourcesToExclude)) { clearAllCachesExceptManeuvers(); From b5e15f52494748a34ab88568782e1d01bbc16a13 Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 3 Dec 2015 18:06:56 +0100 Subject: [PATCH 098/306] Changed from repetitions for the scheduleRepeating to a duration an redid the new position calculation code, now animating towards the next position --- .../shared/racemap/AdvantageLineAnimator.java | 64 +++++++++---------- .../gwt/ui/client/shared/racemap/RaceMap.java | 7 +- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java index 01c33e9bbeb..2cb0788f0ed 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java @@ -4,60 +4,58 @@ import com.google.gwt.maps.client.base.LatLng; import com.google.gwt.maps.client.mvc.MVCArray; import com.google.gwt.maps.client.overlays.Polyline; import com.google.gwt.user.client.Timer; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.impl.DegreeBearingImpl; -import com.sap.sailing.domain.common.impl.NauticalMileDistance; -import com.sap.sailing.gwt.ui.shared.GPSFixDTO; class AdvantageLineAnimator extends Timer { - private static final double MILLIS_TO_HOURS_FACTOR = 3600000; + private static final int ANIMATION_PERIOD = 100; private final Polyline advantageLine; - private GPSFixDTO lastBoatFix; private long lastTime; - private int repetitions = -1; + private int durationMillis = -1; + private MVCArray nextPosition; public AdvantageLineAnimator(Polyline advantageLine) { this.advantageLine = advantageLine; } - public void setLastFix(GPSFixDTO lastBoatFix) { - this.lastBoatFix = lastBoatFix; - scheduleRepeating(100, 10); + public void setNextPosition(MVCArray nextPosition) { + this.nextPosition = nextPosition; + scheduleRepeating(ANIMATION_PERIOD, 1000); //TODO: calculate the duration as Boats do } - public void scheduleRepeating(int periodMillis, int repetitions) { // TODO Jonas: use duration instead of repetitions and check time as we go + public void scheduleRepeating(int periodMillis, int durationMillis) { this.lastTime = System.currentTimeMillis(); this.scheduleRepeating(periodMillis); - this.repetitions = repetitions; + this.durationMillis = durationMillis; } @Override public void run() { - if (repetitions > 0) { - repetitions--; - } else if (repetitions == 0) { // TODO Jonas: see above, use duration / target time + long time = System.currentTimeMillis(); + long deltaTime = time - lastTime; + lastTime = time; + double moveFactor = 1; + if (durationMillis > 0) { + long oldDurationMillis = durationMillis; + durationMillis -= deltaTime; + if (durationMillis < 0) { + deltaTime = oldDurationMillis; + durationMillis = 0; + } + moveFactor = (double)deltaTime / oldDurationMillis; + } else if (durationMillis == 0) { this.cancel(); return; } - if (advantageLine != null) { - long time = System.currentTimeMillis(); - long deltaTime = time - lastTime; - lastTime = time; - if (lastBoatFix != null) { - Position oldPosition = lastBoatFix.position; - Position newPosition = oldPosition.translateRhumb( // TODO Jonas: use great circle navigation - new DegreeBearingImpl(lastBoatFix.speedWithBearing.bearingInDegrees), - new NauticalMileDistance(deltaTime / MILLIS_TO_HOURS_FACTOR * lastBoatFix.speedWithBearing.speedInKnots)); - - final double latDelta = newPosition.getLatDeg() - oldPosition.getLatDeg(); // FIXME Jonas: this would be a delta in "logical coordinates" but not Google Map coordinates - final double lngDelta = newPosition.getLngDeg() - oldPosition.getLngDeg(); - - final MVCArray path = this.advantageLine.getPath(); - LatLng pos1 = path.pop(); - LatLng pos2 = path.pop(); - path.push(LatLng.newInstance(pos2.getLatitude() + latDelta, pos2.getLongitude() + lngDelta)); // FIXME Jonas: must not use Position's lat/lng to draw on map; always use coordinateSystem to map - path.push(LatLng.newInstance(pos1.getLatitude() + latDelta, pos1.getLongitude() + lngDelta)); + if (advantageLine != null && nextPosition != null) { + final MVCArray currentPosition = this.advantageLine.getPath(); + + LatLng currentLatLng; + LatLng nextLatLng; + for(int i = 0; i <= 1; i++) { + currentLatLng = currentPosition.get(i); + nextLatLng = nextPosition.get(i); + currentPosition.setAt(i, LatLng.newInstance(currentLatLng.getLatitude() + (nextLatLng.getLatitude() - currentLatLng.getLatitude()) * moveFactor, + currentLatLng.getLongitude() + (nextLatLng.getLongitude() - currentLatLng.getLongitude()) * moveFactor)); } } } 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 97a78531dc7..36237cbc904 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 @@ -1289,6 +1289,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe } break; } + MVCArray nextPath = MVCArray.newInstance(); LatLng advantageLinePos1 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, coordinateSystem.mapDegreeBearing(rotatedBearingDeg1), advantageLineLengthInKm / 2.0); LatLng advantageLinePos2 = calculatePositionAlongRhumbline(posAheadOfFirstBoat, @@ -1320,13 +1321,13 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe } }); } else { - advantageLine.getPath().setAt(0, advantageLinePos1); - advantageLine.getPath().setAt(1, advantageLinePos2); + nextPath.push(advantageLinePos1); + nextPath.push(advantageLinePos2); + advantageTimer.setNextPosition(nextPath); advantageLineMouseOverHandler.setTrueWindBearing(bearingOfCombinedWindInDeg); advantageLineMouseOverHandler.setDate(new Date(windFix.measureTimepoint)); } drawAdvantageLine = true; - advantageTimer.setLastFix(lastBoatFix); } } if (!drawAdvantageLine) { From 6b839d840d3e9c742019d338cc6661678af9e845 Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 3 Dec 2015 18:21:38 +0100 Subject: [PATCH 099/306] use timeForPositionTransitionMillis for the advantage line animator --- .../ui/client/shared/racemap/AdvantageLineAnimator.java | 8 ++++---- .../sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java index 2cb0788f0ed..444739cac78 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java @@ -10,19 +10,19 @@ class AdvantageLineAnimator extends Timer { private final Polyline advantageLine; private long lastTime; - private int durationMillis = -1; + private long durationMillis = -1; private MVCArray nextPosition; public AdvantageLineAnimator(Polyline advantageLine) { this.advantageLine = advantageLine; } - public void setNextPosition(MVCArray nextPosition) { + public void setNextPositionAndTransitionMillis(MVCArray nextPosition, long timeForPositionTransitionMillis) { this.nextPosition = nextPosition; - scheduleRepeating(ANIMATION_PERIOD, 1000); //TODO: calculate the duration as Boats do + scheduleRepeating(ANIMATION_PERIOD, timeForPositionTransitionMillis); } - public void scheduleRepeating(int periodMillis, int durationMillis) { + public void scheduleRepeating(int periodMillis, long durationMillis) { this.lastTime = System.currentTimeMillis(); this.scheduleRepeating(periodMillis); this.durationMillis = durationMillis; 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 36237cbc904..b83086291ae 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 @@ -917,7 +917,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); // 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); + showAdvantageLine(competitorsToShow, newTime, timeForPositionTransitionMillis); // Rezoom the map LatLngBounds zoomToBounds = null; @@ -1223,7 +1223,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe return null; } - private void showAdvantageLine(Iterable competitorsToShow, Date date) { + 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)) { @@ -1323,7 +1323,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe } else { nextPath.push(advantageLinePos1); nextPath.push(advantageLinePos2); - advantageTimer.setNextPosition(nextPath); + advantageTimer.setNextPositionAndTransitionMillis(nextPath, timeForPositionTransitionMillis); advantageLineMouseOverHandler.setTrueWindBearing(bearingOfCombinedWindInDeg); advantageLineMouseOverHandler.setDate(new Date(windFix.measureTimepoint)); } From 6744f9b89d24723a67fd5f7aaad9922f5e2c7677 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Fri, 4 Dec 2015 12:38:51 +0100 Subject: [PATCH 100/306] newSelectionModel: made RefreshableSelectionModel a possible listener of ListDataProvider --- .../RefreshableMultiSelectionModel.java | 129 ++++++++++++++---- .../celltable/RefreshableSelectionModel.java | 6 +- .../RefreshableSingleSelectionModel.java | 102 +++++++++++--- 3 files changed, 186 insertions(+), 51 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 61dc25de829..756b337653b 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -1,5 +1,7 @@ package com.sap.sse.gwt.client.celltable; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -18,45 +20,39 @@ import com.google.gwt.view.client.SelectionModel; * This {@link RefreshableMultiSelectionModel} have the property that it is refreshable. That means if the * {@link CellTable} have new Elements you can call * {@link RefreshableMultiSelectionModel#refreshSelectionModel(Iterable) refreshSelectionModel(Iterable)} and the - * elements with {@link EntityIdentityComparator#representSameEntity(Object, Object) the same identity} will be reselected. + * elements with {@link EntityIdentityComparator#representSameEntity(Object, Object) the same identity} will be + * reselected. * * @param * the type of {@link CellTable} entries + * + * TODO /FIXME Describe interaction with ListDataProvider */ -public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel, HasData { +public class RefreshableMultiSelectionModel extends MultiSelectionModel + implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; - private final ListDataProvider provider; + private final List elements; /** * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects - * @param provider - * {@link ListDataProvider} to register this as listener and get the entries */ - public RefreshableMultiSelectionModel(EntityIdentityComparator comp, ListDataProvider provider) { + public RefreshableMultiSelectionModel(EntityIdentityComparator comp) { super(); this.comp = comp; - this.provider = provider; - if (this.provider != null) { - this.provider.addDataDisplay(this); - } + elements = new ArrayList<>(); } - + /** * @param keyProvider * {@link ProvidesKey} for the super class constructor * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects - * @param provider - * {@link ListDataProvider} to register this as listener and get the entries */ - public RefreshableMultiSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp, ListDataProvider provider) { + public RefreshableMultiSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { super(keyProvider); this.comp = comp; - this.provider = provider; - if (this.provider != null) { - this.provider.addDataDisplay(this); - } + elements = new ArrayList<>(); } @Override @@ -76,13 +72,14 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im T wasSelectedBefore = null; Set selectedSet = getSelectedSet(); for (T it : selectedSet) { - if(comp.representSameEntity(it, item)) { + if (comp.representSameEntity(it, item)) { wasSelectedBefore = it; break; } } - if(wasSelectedBefore == null) { - super.setSelected(wasSelectedBefore, false); //This old version of item will be deleted with the next clear() + if (wasSelectedBefore != null) { + super.setSelected(wasSelectedBefore, false); + isSelected(item); //triggers the deleting of the wasSelectedBefrore element in super class super.setSelected(item, selected); } else { super.setSelected(item, selected); @@ -90,7 +87,11 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im } } - @Override + /** + * refreshes the {@link RefreshableMultiSelectionModel SelectionModel} with all elements of the {@link ListDataProvider} + *

    + * FIXME change to private + */ public void refreshSelectionModel(Iterable newObjects) { final Set selectedSet = getSelectedSet(); final boolean isNotEmpty = !selectedSet.isEmpty(); @@ -98,7 +99,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im if (isNotEmpty) { for (T it : newObjects) { if (comp == null) { - setSelected(it, selectedSet.contains(it)); + super.setSelected(it, selectedSet.contains(it)); } else { boolean isSelected = false; for (T selected : selectedSet) { @@ -107,18 +108,78 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im break; } } - setSelected(it, isSelected); + super.setSelected(it, isSelected); } } SelectionChangeEvent.fire(this); } } - + + /** + * refreshes the {@link RefreshableMultiSelectionModel SelectionModel}-Part from start to + * elements.size() + * + * @param start + * @param newObjects + */ + @Override + public void refreshSelectionModel(int start, List newObjects) { + List oldElements = elements.subList(start, elements.size()); + Set selectedElements = new HashSet(); + for (T it : oldElements) { + if (isSelected(it)) { + selectedElements.add(it); + super.setSelected(it, false); + } + } + if (!selectedElements.isEmpty()) { + for (T newElement : newObjects) { + if (comp != null) { + for (T selected : selectedElements) { + if (comp.representSameEntity(selected, newElement)) { + super.setSelected(newElement, true); + break; + } + } + } else { + if (selectedElements.contains(newElement)) { + super.setSelected(newElement, true); + } + } + } + SelectionChangeEvent.fire(this); + } + isSelected(newObjects.iterator().next()); // Triggers a clear of unselected elements in superclass + } + + @SuppressWarnings("unchecked") @Override public void setRowData(int start, List values) { - refreshSelectionModel(provider.getList()); + // refresh the selectionModel + if (start > 0 && values.size() > 0) { + refreshSelectionModel(start, (List) values); + } else { + refreshSelectionModel((Iterable) values); + } + // refresh the element list + // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider + if (values.isEmpty()) { + elements.clear(); + } else if (elements.isEmpty()) { + elements.addAll(values); + } else if (values.size() == 1) { + elements.set(start, values.get(0)); + } else if (start == 0) { + elements.clear(); + elements.addAll(values); + } else { + for (int i = start; i < elements.size(); i++) { + elements.remove(i); + } + elements.addAll(values); + } } - + @Override public int getRowCount() { return Integer.MAX_VALUE; @@ -133,56 +194,68 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel im public int getVisibleItemCount() { return Integer.MAX_VALUE; } - + @Override public HandlerRegistration addRangeChangeHandler(Handler handler) { return null; } + @Override public HandlerRegistration addRowCountChangeHandler( com.google.gwt.view.client.RowCountChangeEvent.Handler handler) { return null; } + @Override public boolean isRowCountExact() { return false; } + @Override public void setRowCount(int count) { return; } + @Override public void setRowCount(int count, boolean isExact) { return; } + @Override public void setVisibleRange(int start, int length) { return; } + @Override public void setVisibleRange(Range range) { return; } + @Override public HandlerRegistration addCellPreviewHandler(com.google.gwt.view.client.CellPreviewEvent.Handler handler) { return null; } + @Override public SelectionModel getSelectionModel() { return null; } + @Override public T getVisibleItem(int indexOnPage) { return null; } + @Override public Iterable getVisibleItems() { return null; } + @Override public void setSelectionModel(SelectionModel selectionModel) { return; } + @Override public void setVisibleRangeAndClearData(Range range, boolean forceRangeChangeEvent) { clear(); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java index a38dfd576c5..6cc85922787 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java @@ -1,7 +1,8 @@ package com.sap.sse.gwt.client.celltable; +import java.util.List; + import com.google.gwt.view.client.ListDataProvider; -import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SetSelectionModel; import com.google.gwt.view.client.SingleSelectionModel; @@ -38,5 +39,8 @@ public interface RefreshableSelectionModel extends SetSelectionModel { * {@link SelectionChangeEvent.Handler#onSelectionChange(SelectionChangeEvent) onSelectionChangedEvent} using * {@link AbstractSelectionModel#fireEvent(com.google.gwt.event.shared.GwtEvent)}. */ + // consider deleting this method and switch to private in sub classes public void refreshSelectionModel(Iterable newObjects); + // consider deleting this method and switch to private in sub classes + public void refreshSelectionModel(int start, List newObjects); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index 44a9c9e6bed..df56083ca7a 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -1,10 +1,9 @@ package com.sap.sse.gwt.client.celltable; +import java.util.ArrayList; import java.util.List; - import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.view.client.HasData; -import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.ProvidesKey; import com.google.gwt.view.client.Range; import com.google.gwt.view.client.SelectionChangeEvent; @@ -14,25 +13,22 @@ import com.google.gwt.view.client.RangeChangeEvent.Handler; /** * TODO Lukas: Add Javadoc + * + * * TODO /FIXME Describe interaction with ListDataProvider */ public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; - private final ListDataProvider provider; + private final List elements; /** * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects - * @param provider - * {@link ListDataProvider} to register this as listener and get the entries */ - public RefreshableSingleSelectionModel(EntityIdentityComparator comp, ListDataProvider provider) { + public RefreshableSingleSelectionModel(EntityIdentityComparator comp) { super(); this.comp=comp; - this.provider = provider; - if (this.provider != null) { - this.provider.addDataDisplay(this); - } + elements = new ArrayList<>(); } /** @@ -40,16 +36,11 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel * {@link ProvidesKey} for the super class constructor * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects - * @param provider - * {@link ListDataProvider} to register this as listener and get the entries */ - public RefreshableSingleSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp, ListDataProvider provider) { + public RefreshableSingleSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { super(keyProvider); this.comp =comp; - this.provider = provider; - if (this.provider != null) { - this.provider.addDataDisplay(this); - } + elements = new ArrayList<>(); } @Override @@ -70,7 +61,6 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel } } } - @Override public void refreshSelectionModel(Iterable newObjects) { final T selected = getSelectedObject(); @@ -79,7 +69,7 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel for (final T it : newObjects) { boolean isEqual = comp == null ? selected.equals(it) : comp.representSameEntity(selected, it); if (isEqual) { - setSelected(it, true); + super.setSelected(it, true); break; } } @@ -87,11 +77,67 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel SelectionChangeEvent.fire(this); } + /** + * refreshes the {@link RefreshableMultiSelectionModel SelectionModel}-Part from start to + * elements.size() + * + * @param start + * @param newObjects + */ + @Override + public void refreshSelectionModel(int start, List newObjects) { + List oldElements = elements.subList(start, elements.size()); + T selectedElement = null; + for (T it : oldElements) { + if (isSelected(it)) { + selectedElement = it; + super.setSelected(it, false); + break; + } + } + if (selectedElement != null) { + if (comp != null) { + for (T newElement : newObjects) { + if (comp.representSameEntity(selectedElement, newElement)) { + super.setSelected(newElement, true); + break; + } + } + } else if (newObjects.contains(selectedElement)) { + super.setSelected(newObjects.get(newObjects.indexOf(selectedElement)), true); + } + SelectionChangeEvent.fire(this); + } + } + + @SuppressWarnings("unchecked") @Override public void setRowData(int start, List values) { - refreshSelectionModel(provider.getList()); + // refresh the selectionModel + if (start > 0 && values.size() > 0) { + refreshSelectionModel(start, (List) values); + } else { + refreshSelectionModel((Iterable) values); + } + // refresh the element list + // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider + if (values.isEmpty()) { + elements.clear(); + } else if (elements.isEmpty()) { + elements.addAll(values); + } else if (values.size() == 1) { + elements.set(start, values.get(0)); + } else if (start == 0) { + elements.clear(); + elements.addAll(values); + } else { + for (int i = start; i < elements.size(); i++) { + elements.remove(i); + } + elements.addAll(values); + } } - + @Override public int getRowCount() { return Integer.MAX_VALUE; @@ -106,56 +152,68 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel public int getVisibleItemCount() { return Integer.MAX_VALUE; } - + @Override public HandlerRegistration addRangeChangeHandler(Handler handler) { return null; } + @Override public HandlerRegistration addRowCountChangeHandler( com.google.gwt.view.client.RowCountChangeEvent.Handler handler) { return null; } + @Override public boolean isRowCountExact() { return false; } + @Override public void setRowCount(int count) { return; } + @Override public void setRowCount(int count, boolean isExact) { return; } + @Override public void setVisibleRange(int start, int length) { return; } + @Override public void setVisibleRange(Range range) { return; } + @Override public HandlerRegistration addCellPreviewHandler(com.google.gwt.view.client.CellPreviewEvent.Handler handler) { return null; } + @Override public SelectionModel getSelectionModel() { return null; } + @Override public T getVisibleItem(int indexOnPage) { return null; } + @Override public Iterable getVisibleItems() { return null; } + @Override public void setSelectionModel(SelectionModel selectionModel) { return; } + @Override public void setVisibleRangeAndClearData(Range range, boolean forceRangeChangeEvent) { clear(); From b46e538593c919233e97df25907bf60e3324d906 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 4 Dec 2015 15:26:18 +0100 Subject: [PATCH 101/306] added new type field also to AbstractLeaderboardDTO.hashCode (it was already added to equals before) --- .../sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java | 1 + 1 file changed, 1 insertion(+) diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java index 779d835d434..34947c54603 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java @@ -348,6 +348,7 @@ public abstract class AbstractLeaderboardDTO implements Serializable { result = prime * result + ((competitorDisplayNames == null) ? 0 : competitorDisplayNames.hashCode()); result = prime * result + Arrays.hashCode(discardThresholds); result = prime * result + (hasCarriedPoints ? 1231 : 1237); + result = prime * result + ((type == null) ? 0 : type.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + ((scoringScheme == null) ? 0 : scoringScheme.hashCode()); if (races == null) { From bc78f2773b1f78cb945da51bff863d25cb315964 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 4 Dec 2015 15:40:34 +0100 Subject: [PATCH 102/306] made LeaderboardDTO constructor used only by IncrementalLeaderboardDTO protected; a few whitespace changes --- .../sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java | 1 - .../src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java | 3 +-- .../com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java index 34947c54603..ff197288789 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java @@ -165,7 +165,6 @@ public abstract class AbstractLeaderboardDTO implements Serializable { * {@link RaceColumnInSeriesDTO} * @param fleetDTO * must not be null - * @param isMetaLeaderboardColumn */ public RaceColumnDTO addRace(String raceColumnName, Double explicitFactor, double effectiveFactor, String regattaName, String seriesName, FleetDTO fleetDTO, boolean medalRace, diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java index 64012fec26c..dd9b5e94981 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java @@ -62,7 +62,6 @@ public class LeaderboardDTO extends AbstractLeaderboardDTO implements Serializab /** * @param uuidGenerator used to provide the {@link #id ID} for this object (see also {@link #getId()}) and for any clones produced * from it by the {@link #clone()} operation. - * @param hasOverallDetails TODO */ public LeaderboardDTO(Date timePointOfLastCorrectionsValidity, String comment, ScoringSchemeType scoringScheme, boolean higherScoreIsBetter, UUIDGenerator uuidGenerator, boolean hasOverallDetails) { initCollections(); @@ -79,7 +78,7 @@ public class LeaderboardDTO extends AbstractLeaderboardDTO implements Serializab this.suppressedCompetitors = new ArrayList(); } - public LeaderboardDTO(String id) { + protected LeaderboardDTO(String id) { initCollections(); this.id = id; } 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 0a8679a60d3..3404041d0fb 100755 --- 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 @@ -3239,7 +3239,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS List> liveRaces = leaderboard.getLiveRaces(timer.getLiveTimePointInMillis()); boolean isMeta = leaderboard.type.isMetaLeaderboard(); if (!liveRaces.isEmpty()) { - if(liveRaces.size() == 1) { + if (liveRaces.size() == 1) { com.sap.sse.common.Util.Pair liveRace = liveRaces.get(0); String text = "'" + liveRace.getA().getRaceColumnName() + "'"; result = isMeta ? stringMessages.regattaIsLive(text) : stringMessages.raceIsLive(text); From 250b91232843ad2ba427882ebe94198e5f39115f Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 4 Dec 2015 16:14:42 +0100 Subject: [PATCH 103/306] splitting RaceDTO into BasicRaceDTO and RaceDTO where BasicRaceDTO has all it takes for isLive check, making MetaLeaderboardRaceColumnDTO smaller --- .../domain/common/dto/BasicRaceDTO.java | 120 ++++++++++++++++++ .../dto/MetaLeaderboardRaceColumnDTO.java | 4 +- .../sailing/domain/common/dto/RaceDTO.java | 75 +---------- 3 files changed, 124 insertions(+), 75 deletions(-) create mode 100644 java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java new file mode 100644 index 00000000000..aca289a1162 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java @@ -0,0 +1,120 @@ +package com.sap.sailing.domain.common.dto; + +import java.util.Date; + +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.TimingConstants; + +/** + * Master data about a single race that is to be transferred to the client.

    + * + * @author Axel Uhl (d043530) + * + */ +public class BasicRaceDTO extends NamedDTO { + private static final long serialVersionUID = 2613189982608149975L; + + /** + * Tells if this race is currently being tracked, meaning that a {@link RaceTracker} is + * listening for incoming GPS fixes, mark passings etc., to update a {@link TrackedRace} object + * accordingly. + */ + public boolean isTracked; + + public Date startOfRace; + public Date endOfRace; + public TrackedRaceDTO trackedRace; + + public BasicRaceDTO() {} // for GWT serialization only + + public BasicRaceDTO(RegattaAndRaceIdentifier raceIdentifier, TrackedRaceDTO trackedRace) { + super(raceIdentifier.getRaceName()); + this.trackedRace = trackedRace; + } + + /** + * @see {@link TrackedRace#isLive} for further explanation. + * @param serverTimePointAsMillis + * the time point (in server clock time) at which to determine whether the race is/was live + * @return true if serverTimePointAsMillis is between (inclusively) the start and end time + * point of the "live" interval as defined above. + */ + public boolean isLive(long serverTimePointAsMillis) { + final Date startOfLivePeriod; + final Date endOfLivePeriod; + if (trackedRace == null || !trackedRace.hasGPSData || !trackedRace.hasWindData) { + startOfLivePeriod = null; + endOfLivePeriod = null; + } else { + if (startOfRace == null) { + startOfLivePeriod = trackedRace.startOfTracking; + } else { + startOfLivePeriod = new Date(startOfRace.getTime() - TimingConstants.PRE_START_PHASE_DURATION_IN_MILLIS); + } + if (endOfRace == null) { + if (trackedRace.timePointOfNewestEvent != null) { + endOfLivePeriod = new Date(trackedRace.timePointOfNewestEvent.getTime() + + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); + } else { + endOfLivePeriod = null; + } + } else { + endOfLivePeriod = new Date(endOfRace.getTime() + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); + } + } + + // if an empty timepoint is given then take the start of the race + if (serverTimePointAsMillis == 0) { + serverTimePointAsMillis = startOfLivePeriod.getTime()+1; + } + + // whenLastTrackedRaceWasLive is null if there is no tracked race for fleet, or the tracked race hasn't started yet at the server time + // when this DTO was assembled, or there were no GPS or wind data + final boolean result = + startOfLivePeriod != null && + endOfLivePeriod != null && + startOfLivePeriod.getTime() <= serverTimePointAsMillis && + serverTimePointAsMillis <= endOfLivePeriod.getTime(); + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((endOfRace == null) ? 0 : endOfRace.hashCode()); + result = prime * result + (isTracked ? 1231 : 1237); + result = prime * result + ((startOfRace == null) ? 0 : startOfRace.hashCode()); + result = prime * result + ((trackedRace == null) ? 0 : trackedRace.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + BasicRaceDTO other = (BasicRaceDTO) obj; + if (endOfRace == null) { + if (other.endOfRace != null) + return false; + } else if (!endOfRace.equals(other.endOfRace)) + return false; + if (isTracked != other.isTracked) + return false; + if (startOfRace == null) { + if (other.startOfRace != null) + return false; + } else if (!startOfRace.equals(other.startOfRace)) + return false; + if (trackedRace == null) { + if (other.trackedRace != null) + return false; + } else if (!trackedRace.equals(other.trackedRace)) + return false; + return true; + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java index 16cc926d0e0..6678dbdd683 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java @@ -6,10 +6,10 @@ public class MetaLeaderboardRaceColumnDTO extends RaceColumnDTO { private static final long serialVersionUID = 6152752963316150432L; - private ArrayList raceList = new ArrayList(); + private ArrayList raceList = new ArrayList(); public boolean isLive(FleetDTO fleet, long serverTimePointAsMillis) { - for (RaceDTO race : raceList) { + for (BasicRaceDTO race : raceList) { if (race.isLive(serverTimePointAsMillis)) { return true; } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java index eba9caf39b7..ec90d976adb 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java @@ -1,10 +1,7 @@ package com.sap.sailing.domain.common.dto; -import java.util.Date; - import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.TimingConstants; /** * Master data about a single race that is to be transferred to the client.

    @@ -12,7 +9,7 @@ import com.sap.sailing.domain.common.TimingConstants; * @author Axel Uhl (d043530) * */ -public class RaceDTO extends NamedDTO { +public class RaceDTO extends BasicRaceDTO { private static final long serialVersionUID = 2613189982608149975L; public enum RaceLiveState { NOT_TRACKED, TRACKED, TRACKED_AND_LIVE, TRACKED_BUT_NOT_SCHEDULED }; @@ -24,13 +21,10 @@ public class RaceDTO extends NamedDTO { */ public boolean isTracked; - public Date startOfRace; - public Date endOfRace; public RaceStatusDTO status; public PlacemarkOrderDTO places; - public TrackedRaceDTO trackedRace; public TrackedRaceStatisticsDTO trackedRaceStatistics; private String regattaName; @@ -43,9 +37,8 @@ public class RaceDTO extends NamedDTO { } public RaceDTO(RegattaAndRaceIdentifier raceIdentifier, TrackedRaceDTO trackedRace, boolean isCurrentlyTracked) { - super(raceIdentifier.getRaceName()); + super(raceIdentifier, trackedRace); this.regattaName = raceIdentifier.getRegattaName(); - this.trackedRace = trackedRace; this.isTracked = isCurrentlyTracked; } @@ -71,64 +64,15 @@ public class RaceDTO extends NamedDTO { return regattaName; } - /** - * @see {@link TrackedRace#isLive} for further explanation. - * @param serverTimePointAsMillis - * the time point (in server clock time) at which to determine whether the race is/was live - * @return true if serverTimePointAsMillis is between (inclusively) the start and end time - * point of the "live" interval as defined above. - */ - public boolean isLive(long serverTimePointAsMillis) { - final Date startOfLivePeriod; - final Date endOfLivePeriod; - if (trackedRace == null || !trackedRace.hasGPSData || !trackedRace.hasWindData) { - startOfLivePeriod = null; - endOfLivePeriod = null; - } else { - if (startOfRace == null) { - startOfLivePeriod = trackedRace.startOfTracking; - } else { - startOfLivePeriod = new Date(startOfRace.getTime() - TimingConstants.PRE_START_PHASE_DURATION_IN_MILLIS); - } - if (endOfRace == null) { - if (trackedRace.timePointOfNewestEvent != null) { - endOfLivePeriod = new Date(trackedRace.timePointOfNewestEvent.getTime() - + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); - } else { - endOfLivePeriod = null; - } - } else { - endOfLivePeriod = new Date(endOfRace.getTime() + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); - } - } - - // if an empty timepoint is given then take the start of the race - if (serverTimePointAsMillis == 0) { - serverTimePointAsMillis = startOfLivePeriod.getTime()+1; - } - - // whenLastTrackedRaceWasLive is null if there is no tracked race for fleet, or the tracked race hasn't started yet at the server time - // when this DTO was assembled, or there were no GPS or wind data - final boolean result = - startOfLivePeriod != null && - endOfLivePeriod != null && - startOfLivePeriod.getTime() <= serverTimePointAsMillis && - serverTimePointAsMillis <= endOfLivePeriod.getTime(); - return result; - } - @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + ((boatClass == null) ? 0 : boatClass.hashCode()); - result = prime * result + ((endOfRace == null) ? 0 : endOfRace.hashCode()); result = prime * result + (isTracked ? 1231 : 1237); result = prime * result + ((places == null) ? 0 : places.hashCode()); result = prime * result + ((regattaName == null) ? 0 : regattaName.hashCode()); - result = prime * result + ((startOfRace == null) ? 0 : startOfRace.hashCode()); result = prime * result + ((status == null) ? 0 : status.hashCode()); - result = prime * result + ((trackedRace == null) ? 0 : trackedRace.hashCode()); return result; } @@ -146,11 +90,6 @@ public class RaceDTO extends NamedDTO { return false; } else if (!boatClass.equals(other.boatClass)) return false; - if (endOfRace == null) { - if (other.endOfRace != null) - return false; - } else if (!endOfRace.equals(other.endOfRace)) - return false; if (isTracked != other.isTracked) return false; if (places == null) { @@ -163,21 +102,11 @@ public class RaceDTO extends NamedDTO { return false; } else if (!regattaName.equals(other.regattaName)) return false; - if (startOfRace == null) { - if (other.startOfRace != null) - return false; - } else if (!startOfRace.equals(other.startOfRace)) - return false; if (status == null) { if (other.status != null) return false; } else if (!status.equals(other.status)) return false; - if (trackedRace == null) { - if (other.trackedRace != null) - return false; - } else if (!trackedRace.equals(other.trackedRace)) - return false; return true; } } From 73b9264404c9e50edc7a0fcd25eed66cde94f5ca Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 4 Dec 2015 16:20:39 +0100 Subject: [PATCH 104/306] use BasicRaceDTO during the construction of the MetaLeaderboardRaceColumnDTO --- .../domain/common/dto/MetaLeaderboardRaceColumnDTO.java | 2 +- .../leaderboard/impl/AbstractSimpleLeaderboardImpl.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java index 6678dbdd683..475718b447b 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java @@ -17,7 +17,7 @@ public class MetaLeaderboardRaceColumnDTO extends RaceColumnDTO { return false; } - public void addRace(RaceDTO race) { + public void addRace(BasicRaceDTO race) { raceList.add(race); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java index 08d5fe903d9..454843ba3e2 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java @@ -52,6 +52,7 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaNameAndRaceName; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.dto.BasicRaceDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.FleetDTO; import com.sap.sailing.domain.common.dto.LeaderboardDTO; @@ -1377,8 +1378,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race String regattaName = trackedRace.getTrackedRegatta().getRegatta().getName(); String raceName = trackedRace.getRace().getName(); RegattaAndRaceIdentifier raceIdentifier = new RegattaNameAndRaceName(regattaName, raceName); - columnDTO.addRace(baseDomainFactory.createRaceDTO(trackedRegattaRegistry, - /* withGeoLocationData */ false, raceIdentifier, trackedRace)); + columnDTO.addRace(new BasicRaceDTO(raceIdentifier, baseDomainFactory.createTrackedRaceDTO(trackedRace))); } } } From a9393f55cf726f7a3e21ba909355a52265683d16 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 4 Dec 2015 16:40:39 +0100 Subject: [PATCH 105/306] removed duplicate field isTracked from BasicRaceDTO --- .../sap/sailing/domain/common/dto/BasicRaceDTO.java | 10 ---------- .../src/com/sap/sailing/domain/common/dto/RaceDTO.java | 10 +++++----- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java index aca289a1162..e48bebff979 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java @@ -14,13 +14,6 @@ import com.sap.sailing.domain.common.TimingConstants; public class BasicRaceDTO extends NamedDTO { private static final long serialVersionUID = 2613189982608149975L; - /** - * Tells if this race is currently being tracked, meaning that a {@link RaceTracker} is - * listening for incoming GPS fixes, mark passings etc., to update a {@link TrackedRace} object - * accordingly. - */ - public boolean isTracked; - public Date startOfRace; public Date endOfRace; public TrackedRaceDTO trackedRace; @@ -83,7 +76,6 @@ public class BasicRaceDTO extends NamedDTO { final int prime = 31; int result = super.hashCode(); result = prime * result + ((endOfRace == null) ? 0 : endOfRace.hashCode()); - result = prime * result + (isTracked ? 1231 : 1237); result = prime * result + ((startOfRace == null) ? 0 : startOfRace.hashCode()); result = prime * result + ((trackedRace == null) ? 0 : trackedRace.hashCode()); return result; @@ -103,8 +95,6 @@ public class BasicRaceDTO extends NamedDTO { return false; } else if (!endOfRace.equals(other.endOfRace)) return false; - if (isTracked != other.isTracked) - return false; if (startOfRace == null) { if (other.startOfRace != null) return false; diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java index ec90d976adb..6a8e90c617f 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java @@ -43,14 +43,14 @@ public class RaceDTO extends BasicRaceDTO { } public RaceLiveState getLiveState(long serverTimePointAsMillis) { - RaceLiveState result = RaceLiveState.NOT_TRACKED; - if(trackedRace != null && trackedRace.hasGPSData && trackedRace.hasWindData) { + RaceLiveState result = RaceLiveState.NOT_TRACKED; + if (trackedRace != null && trackedRace.hasGPSData && trackedRace.hasWindData) { result = RaceLiveState.TRACKED; - if(isLive(serverTimePointAsMillis)) { + if (isLive(serverTimePointAsMillis)) { result = RaceLiveState.TRACKED_AND_LIVE; - if(startOfRace == null) { + if (startOfRace == null) { result = RaceLiveState.TRACKED_BUT_NOT_SCHEDULED; - } + } } } return result; From bb144402203f4397c5d8bb02378e66defe86ce8e Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Fri, 4 Dec 2015 16:49:11 +0100 Subject: [PATCH 106/306] modified RefreshableMultiSelectionModel --- .../RefreshableMultiSelectionModel.java | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 756b337653b..b49938a98bd 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -32,6 +32,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; private final List elements; + private boolean dontcheckSelectionState = false; /** * @param comp @@ -66,7 +67,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel */ @Override public void setSelected(T item, boolean selected) { - if (comp == null) { + if (comp == null || dontcheckSelectionState) { super.setSelected(item, selected); } else { T wasSelectedBefore = null; @@ -93,13 +94,14 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel * FIXME change to private */ public void refreshSelectionModel(Iterable newObjects) { + dontcheckSelectionState = true; final Set selectedSet = getSelectedSet(); final boolean isNotEmpty = !selectedSet.isEmpty(); - clear(); if (isNotEmpty) { + clear(); for (T it : newObjects) { if (comp == null) { - super.setSelected(it, selectedSet.contains(it)); + setSelected(it, selectedSet.contains(it)); } else { boolean isSelected = false; for (T selected : selectedSet) { @@ -108,11 +110,12 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel break; } } - super.setSelected(it, isSelected); + setSelected(it, isSelected); } } SelectionChangeEvent.fire(this); } + dontcheckSelectionState = false; } /** @@ -124,12 +127,13 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel */ @Override public void refreshSelectionModel(int start, List newObjects) { + dontcheckSelectionState = true; List oldElements = elements.subList(start, elements.size()); Set selectedElements = new HashSet(); for (T it : oldElements) { if (isSelected(it)) { selectedElements.add(it); - super.setSelected(it, false); + setSelected(it, false); } } if (!selectedElements.isEmpty()) { @@ -137,46 +141,49 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel if (comp != null) { for (T selected : selectedElements) { if (comp.representSameEntity(selected, newElement)) { - super.setSelected(newElement, true); + setSelected(newElement, true); break; } } } else { if (selectedElements.contains(newElement)) { - super.setSelected(newElement, true); + setSelected(newElement, true); } } } SelectionChangeEvent.fire(this); } isSelected(newObjects.iterator().next()); // Triggers a clear of unselected elements in superclass + dontcheckSelectionState = false; } @SuppressWarnings("unchecked") @Override public void setRowData(int start, List values) { - // refresh the selectionModel - if (start > 0 && values.size() > 0) { - refreshSelectionModel(start, (List) values); - } else { - refreshSelectionModel((Iterable) values); - } - // refresh the element list - // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider - if (values.isEmpty()) { - elements.clear(); - } else if (elements.isEmpty()) { - elements.addAll(values); - } else if (values.size() == 1) { - elements.set(start, values.get(0)); - } else if (start == 0) { - elements.clear(); - elements.addAll(values); - } else { - for (int i = start; i < elements.size(); i++) { - elements.remove(i); + if (!values.equals(elements.subList(start, elements.size()))) { + // refresh the selectionModel + if (start == 0 && (values.size() > 1 || values.size()==0)) { + refreshSelectionModel((Iterable) values); + } else { + refreshSelectionModel(start, (List) values); + } + // refresh the element list + // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider + if (values.isEmpty()) { + elements.clear(); + } else if (elements.isEmpty()) { + elements.addAll(values); + } else if (values.size() == 1) { + elements.set(start, values.get(0)); + } else if (start == 0) { + elements.clear(); + elements.addAll(values); + } else { + for (int i = start; i < elements.size(); i++) { + elements.remove(i); + } + elements.addAll(values); } - elements.addAll(values); } } From f467a2de7d1bef97d7aad1636aacbd180374726d Mon Sep 17 00:00:00 2001 From: Jonas Dann Date: Fri, 4 Dec 2015 17:55:18 +0100 Subject: [PATCH 107/306] fixed problem with animated advantage line when clicking on the compass --- .../gwt/ui/client/shared/racemap/AdvantageLineAnimator.java | 4 ++++ .../com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java | 2 ++ 2 files changed, 6 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java index 444739cac78..10a2acee30f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java @@ -59,4 +59,8 @@ class AdvantageLineAnimator extends Timer { } } } + + public void removeAnimation() { + this.durationMillis = -1; + } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java index b83086291ae..1b79dcbeddb 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 @@ -712,6 +712,8 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe for (CompetitorInfoOverlay infoOverlay : competitorInfoOverlays.values()) { infoOverlay.removeCanvasPositionAndRotationTransition(); } + // remove the advantage line animation + advantageTimer.removeAnimation(); } public void redraw() { From 1c5ab6eb9864e7737fc027181c997466ca274f76 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sun, 6 Dec 2015 00:52:45 +0100 Subject: [PATCH 108/306] fixed serialVersionUID --- .../com/sap/sailing/domain/common/dto/BasicRaceDTO.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java index e48bebff979..8938136783c 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java @@ -6,14 +6,16 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.TimingConstants; /** - * Master data about a single race that is to be transferred to the client.

    + * Master data about a single race that is to be transferred to the client. Holds only timing and a bit + * of information about the tracked race, if any. See also {@link RaceDTO} for a more comprehensive set + * of data about a race to be serialized to a client.

    * * @author Axel Uhl (d043530) * */ public class BasicRaceDTO extends NamedDTO { - private static final long serialVersionUID = 2613189982608149975L; - + private static final long serialVersionUID = -7884808503795229609L; + public Date startOfRace; public Date endOfRace; public TrackedRaceDTO trackedRace; From 625d0baf9268533b1b61339704f31dde0ec65f98 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Sun, 6 Dec 2015 00:53:24 +0100 Subject: [PATCH 109/306] re-generated serialized form of a test object of type IncrementalLeaderboardDTO after splitting up RaceDTO into BasicRaceDTO and RaceDTO --- .../resources/IncrementalLeaderboardDTO.ser | Bin 223080 -> 271138 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/java/com.sap.sailing.domain.test/resources/IncrementalLeaderboardDTO.ser b/java/com.sap.sailing.domain.test/resources/IncrementalLeaderboardDTO.ser index 1fd2fb452b3852aa12c930a50dd7da94142d587e..8b0225928aa4a94d0309a1e921083f65d7f937af 100755 GIT binary patch literal 271138 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

    1%R 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

    >UIwiURVPWTJ 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 literal 223080 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

    @oN;}muveC?4$!-dorBZFPLRxKI;=2KQY~3 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 z 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 YX*Fx5ruC>rwpA;JD*%QoHX{B10ccIKsQ>@~ From b32b8d49cbb87cdc771655a9846015d6da37ed9c Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 7 Dec 2015 09:13:26 +0100 Subject: [PATCH 110/306] newSelectionModel: some optimization in SingleSelectionModel --- .../RefreshableSingleSelectionModel.java | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index df56083ca7a..a3f5074e992 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -20,6 +20,7 @@ import com.google.gwt.view.client.RangeChangeEvent.Handler; public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; private final List elements; + private boolean dontcheckSelectionState = false; /** * @param comp @@ -50,7 +51,7 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel @Override public void setSelected(T item, boolean selected) { - if (comp == null) { + if (comp == null || dontcheckSelectionState) { super.setSelected(item, selected); } else { if(comp.representSameEntity(getSelectedObject(), item)) { @@ -63,18 +64,20 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel } @Override public void refreshSelectionModel(Iterable newObjects) { + dontcheckSelectionState = true; final T selected = getSelectedObject(); - clear(); if (selected != null) { + clear(); for (final T it : newObjects) { boolean isEqual = comp == null ? selected.equals(it) : comp.representSameEntity(selected, it); if (isEqual) { - super.setSelected(it, true); + setSelected(it, true); break; } } } SelectionChangeEvent.fire(this); + dontcheckSelectionState = false; } /** @@ -86,12 +89,13 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel */ @Override public void refreshSelectionModel(int start, List newObjects) { + dontcheckSelectionState = true; List oldElements = elements.subList(start, elements.size()); T selectedElement = null; for (T it : oldElements) { if (isSelected(it)) { selectedElement = it; - super.setSelected(it, false); + setSelected(it, false); break; } } @@ -99,42 +103,45 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel if (comp != null) { for (T newElement : newObjects) { if (comp.representSameEntity(selectedElement, newElement)) { - super.setSelected(newElement, true); + setSelected(newElement, true); break; } } } else if (newObjects.contains(selectedElement)) { - super.setSelected(newObjects.get(newObjects.indexOf(selectedElement)), true); + setSelected(newObjects.get(newObjects.indexOf(selectedElement)), true); } SelectionChangeEvent.fire(this); } + dontcheckSelectionState = false; } @SuppressWarnings("unchecked") @Override public void setRowData(int start, List values) { - // refresh the selectionModel - if (start > 0 && values.size() > 0) { - refreshSelectionModel(start, (List) values); - } else { - refreshSelectionModel((Iterable) values); - } - // refresh the element list - // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider - if (values.isEmpty()) { - elements.clear(); - } else if (elements.isEmpty()) { - elements.addAll(values); - } else if (values.size() == 1) { - elements.set(start, values.get(0)); - } else if (start == 0) { - elements.clear(); - elements.addAll(values); - } else { - for (int i = start; i < elements.size(); i++) { - elements.remove(i); + if (!values.equals(elements.subList(start, elements.size()))) { + // refresh the selectionModel + if (start == 0 && (values.size() > 1 || values.size() == 0)) { + refreshSelectionModel((Iterable) values); + } else { + refreshSelectionModel(start, (List) values); + } + // refresh the element list + // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider + if (values.isEmpty()) { + elements.clear(); + } else if (elements.isEmpty()) { + elements.addAll(values); + } else if (values.size() == 1) { + elements.set(start, values.get(0)); + } else if (start == 0) { + elements.clear(); + elements.addAll(values); + } else { + for (int i = start; i < elements.size(); i++) { + elements.remove(i); + } + elements.addAll(values); } - elements.addAll(values); } } From afae0e480aabe36007d75c5a92c615dc32a5bf41 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 7 Dec 2015 09:46:25 +0100 Subject: [PATCH 111/306] some changes in RefreshableSelectionModels --- .../celltable/RefreshableMultiSelectionModel.java | 6 +++--- .../RefreshableSingleSelectionModel.java | 15 ++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index b49938a98bd..3f33444935b 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -96,8 +96,8 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel public void refreshSelectionModel(Iterable newObjects) { dontcheckSelectionState = true; final Set selectedSet = getSelectedSet(); - final boolean isNotEmpty = !selectedSet.isEmpty(); - if (isNotEmpty) { + final boolean isEmpty = selectedSet.isEmpty(); + if (!isEmpty) { clear(); for (T it : newObjects) { if (comp == null) { @@ -107,10 +107,10 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel for (T selected : selectedSet) { isSelected = comp.representSameEntity(selected, it); if (isSelected) { + setSelected(it, isSelected); break; } } - setSelected(it, isSelected); } } SelectionChangeEvent.fire(this); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index a3f5074e992..b2599062ca4 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -90,16 +90,13 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel @Override public void refreshSelectionModel(int start, List newObjects) { dontcheckSelectionState = true; - List oldElements = elements.subList(start, elements.size()); - T selectedElement = null; - for (T it : oldElements) { - if (isSelected(it)) { - selectedElement = it; - setSelected(it, false); - break; - } - } + T selectedElement = getSelectedObject(); if (selectedElement != null) { + final int index = elements.indexOf(selectedElement); + if (index < 0 || index < start) { + return; + } + setSelected(selectedElement, false); if (comp != null) { for (T newElement : newObjects) { if (comp.representSameEntity(selectedElement, newElement)) { From aab807ec021eab3a1a819b283597cc1575238671 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 7 Dec 2015 15:45:28 +0100 Subject: [PATCH 112/306] newSelectionModel: created FlushableCellTable.java & made UserManagementPanel.java --- .../ui/client/shared/controls/Flushable.java | 20 +++++ .../shared/controls/FlushableCellTable.java | 32 ++++++++ .../RefreshableMultiSelectionModel.java | 67 +++++++++-------- .../RefreshableSingleSelectionModel.java | 74 ++++++++++--------- .../client/component/UserManagementPanel.java | 30 +++++--- 5 files changed, 147 insertions(+), 76 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/Flushable.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableCellTable.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/Flushable.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/Flushable.java new file mode 100644 index 00000000000..17f58746c5b --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/Flushable.java @@ -0,0 +1,20 @@ +package com.sap.sailing.gwt.ui.client.shared.controls; + +import com.google.gwt.user.cellview.client.CellTable; +import com.google.gwt.view.client.HasData; + +/** + * This {@link Flushable}-interface should be implemented of the {@link HasData display} which uses the + * {@link SelectionCheckboxColumn}. You need this interface to ensure that the display, displays the correct selection + * state of the {@link SelectionCheckboxColumn}. The method {@link Flushable#flush()} will trigger a redraw of selected + * lines e.g. with calling the method {@link CellTable#flush()}. + * + * @author D064976 + */ +public interface Flushable { + /** + * This method will trigger a redraw of the {@link HasData display} so the {@link SelectionCheckboxColumn} is shown + * correctly again. This is especially imported when the selection comes form the program and not from the user. + */ + public void flush(); +} \ 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/controls/FlushableCellTable.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableCellTable.java new file mode 100644 index 00000000000..65e77cb8f72 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableCellTable.java @@ -0,0 +1,32 @@ +package com.sap.sailing.gwt.ui.client.shared.controls; + +import com.google.gwt.user.cellview.client.CellTable; +import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.ProvidesKey; + +public class FlushableCellTable extends CellTable implements Flushable { + public FlushableCellTable() { + super(); + } + public FlushableCellTable(final int pageSize) { + super(pageSize); + } + public FlushableCellTable(ProvidesKey keyProvider) { + super(keyProvider); + } + public FlushableCellTable(int pageSize, Resources resources) { + super(pageSize, resources); + } + public FlushableCellTable(int pageSize, ProvidesKey keyProvider) { + super(pageSize, keyProvider); + } + public FlushableCellTable(final int pageSize, Resources resources, ProvidesKey keyPrivider) { + super(pageSize, resources, keyPrivider); + } + public FlushableCellTable(final int pageSize, Resources resources, ProvidesKey keyProvider, Widget loadingIndicator) { + super(pageSize, resources, keyProvider, loadingIndicator); + } + public FlushableCellTable(final int pageSize, Resources resources, ProvidesKey keyProvider, Widget loadingIndicator, boolean enableColGroup, boolean attachLoadingPanel) { + super(pageSize,resources,keyProvider,loadingIndicator,enableColGroup, attachLoadingPanel); + } +} \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 756b337653b..3f33444935b 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -32,6 +32,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; private final List elements; + private boolean dontcheckSelectionState = false; /** * @param comp @@ -66,7 +67,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel */ @Override public void setSelected(T item, boolean selected) { - if (comp == null) { + if (comp == null || dontcheckSelectionState) { super.setSelected(item, selected); } else { T wasSelectedBefore = null; @@ -93,26 +94,28 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel * FIXME change to private */ public void refreshSelectionModel(Iterable newObjects) { + dontcheckSelectionState = true; final Set selectedSet = getSelectedSet(); - final boolean isNotEmpty = !selectedSet.isEmpty(); - clear(); - if (isNotEmpty) { + final boolean isEmpty = selectedSet.isEmpty(); + if (!isEmpty) { + clear(); for (T it : newObjects) { if (comp == null) { - super.setSelected(it, selectedSet.contains(it)); + setSelected(it, selectedSet.contains(it)); } else { boolean isSelected = false; for (T selected : selectedSet) { isSelected = comp.representSameEntity(selected, it); if (isSelected) { + setSelected(it, isSelected); break; } } - super.setSelected(it, isSelected); } } SelectionChangeEvent.fire(this); } + dontcheckSelectionState = false; } /** @@ -124,12 +127,13 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel */ @Override public void refreshSelectionModel(int start, List newObjects) { + dontcheckSelectionState = true; List oldElements = elements.subList(start, elements.size()); Set selectedElements = new HashSet(); for (T it : oldElements) { if (isSelected(it)) { selectedElements.add(it); - super.setSelected(it, false); + setSelected(it, false); } } if (!selectedElements.isEmpty()) { @@ -137,46 +141,49 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel if (comp != null) { for (T selected : selectedElements) { if (comp.representSameEntity(selected, newElement)) { - super.setSelected(newElement, true); + setSelected(newElement, true); break; } } } else { if (selectedElements.contains(newElement)) { - super.setSelected(newElement, true); + setSelected(newElement, true); } } } SelectionChangeEvent.fire(this); } isSelected(newObjects.iterator().next()); // Triggers a clear of unselected elements in superclass + dontcheckSelectionState = false; } @SuppressWarnings("unchecked") @Override public void setRowData(int start, List values) { - // refresh the selectionModel - if (start > 0 && values.size() > 0) { - refreshSelectionModel(start, (List) values); - } else { - refreshSelectionModel((Iterable) values); - } - // refresh the element list - // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider - if (values.isEmpty()) { - elements.clear(); - } else if (elements.isEmpty()) { - elements.addAll(values); - } else if (values.size() == 1) { - elements.set(start, values.get(0)); - } else if (start == 0) { - elements.clear(); - elements.addAll(values); - } else { - for (int i = start; i < elements.size(); i++) { - elements.remove(i); + if (!values.equals(elements.subList(start, elements.size()))) { + // refresh the selectionModel + if (start == 0 && (values.size() > 1 || values.size()==0)) { + refreshSelectionModel((Iterable) values); + } else { + refreshSelectionModel(start, (List) values); + } + // refresh the element list + // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider + if (values.isEmpty()) { + elements.clear(); + } else if (elements.isEmpty()) { + elements.addAll(values); + } else if (values.size() == 1) { + elements.set(start, values.get(0)); + } else if (start == 0) { + elements.clear(); + elements.addAll(values); + } else { + for (int i = start; i < elements.size(); i++) { + elements.remove(i); + } + elements.addAll(values); } - elements.addAll(values); } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index df56083ca7a..b2599062ca4 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -20,6 +20,7 @@ import com.google.gwt.view.client.RangeChangeEvent.Handler; public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; private final List elements; + private boolean dontcheckSelectionState = false; /** * @param comp @@ -50,7 +51,7 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel @Override public void setSelected(T item, boolean selected) { - if (comp == null) { + if (comp == null || dontcheckSelectionState) { super.setSelected(item, selected); } else { if(comp.representSameEntity(getSelectedObject(), item)) { @@ -63,18 +64,20 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel } @Override public void refreshSelectionModel(Iterable newObjects) { + dontcheckSelectionState = true; final T selected = getSelectedObject(); - clear(); if (selected != null) { + clear(); for (final T it : newObjects) { boolean isEqual = comp == null ? selected.equals(it) : comp.representSameEntity(selected, it); if (isEqual) { - super.setSelected(it, true); + setSelected(it, true); break; } } } SelectionChangeEvent.fire(this); + dontcheckSelectionState = false; } /** @@ -86,55 +89,56 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel */ @Override public void refreshSelectionModel(int start, List newObjects) { - List oldElements = elements.subList(start, elements.size()); - T selectedElement = null; - for (T it : oldElements) { - if (isSelected(it)) { - selectedElement = it; - super.setSelected(it, false); - break; - } - } + dontcheckSelectionState = true; + T selectedElement = getSelectedObject(); if (selectedElement != null) { + final int index = elements.indexOf(selectedElement); + if (index < 0 || index < start) { + return; + } + setSelected(selectedElement, false); if (comp != null) { for (T newElement : newObjects) { if (comp.representSameEntity(selectedElement, newElement)) { - super.setSelected(newElement, true); + setSelected(newElement, true); break; } } } else if (newObjects.contains(selectedElement)) { - super.setSelected(newObjects.get(newObjects.indexOf(selectedElement)), true); + setSelected(newObjects.get(newObjects.indexOf(selectedElement)), true); } SelectionChangeEvent.fire(this); } + dontcheckSelectionState = false; } @SuppressWarnings("unchecked") @Override public void setRowData(int start, List values) { - // refresh the selectionModel - if (start > 0 && values.size() > 0) { - refreshSelectionModel(start, (List) values); - } else { - refreshSelectionModel((Iterable) values); - } - // refresh the element list - // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider - if (values.isEmpty()) { - elements.clear(); - } else if (elements.isEmpty()) { - elements.addAll(values); - } else if (values.size() == 1) { - elements.set(start, values.get(0)); - } else if (start == 0) { - elements.clear(); - elements.addAll(values); - } else { - for (int i = start; i < elements.size(); i++) { - elements.remove(i); + if (!values.equals(elements.subList(start, elements.size()))) { + // refresh the selectionModel + if (start == 0 && (values.size() > 1 || values.size() == 0)) { + refreshSelectionModel((Iterable) values); + } else { + refreshSelectionModel(start, (List) values); + } + // refresh the element list + // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider + if (values.isEmpty()) { + elements.clear(); + } else if (elements.isEmpty()) { + elements.addAll(values); + } else if (values.size() == 1) { + elements.set(start, values.get(0)); + } else if (start == 0) { + elements.clear(); + elements.addAll(values); + } else { + for (int i = start; i < elements.size(); i++) { + elements.remove(i); + } + elements.addAll(values); } - elements.addAll(values); } } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java index 3e44dd457ae..022759c56ac 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java @@ -22,7 +22,8 @@ import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.VerticalPanel; 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.sse.gwt.client.celltable.EntityIdentityComparator; +import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; import com.sap.sse.security.shared.PermissionsForRoleProvider; import com.sap.sse.security.ui.client.UserChangeEventHandler; import com.sap.sse.security.ui.client.UserManagementServiceAsync; @@ -37,7 +38,7 @@ public class UserManagementPanel extends DockPanel { private List userDeletedHandlers = new ArrayList<>(); - private SingleSelectionModel singleSelectionModel; + private RefreshableSingleSelectionModel refreshableSingleSelectionModel; private UserListDataProvider userListDataProvider; @@ -46,7 +47,14 @@ public class UserManagementPanel extends DockPanel { VerticalPanel west = new VerticalPanel(); HorizontalPanel buttonPanel = new HorizontalPanel(); west.add(buttonPanel); - singleSelectionModel = new SingleSelectionModel<>(); + refreshableSingleSelectionModel = new RefreshableSingleSelectionModel<>( + new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(UserDTO dto1, UserDTO dto2) { + return dto1.getEmail().equals(dto2.getEmail()); + } + }); Button createButton = new Button(stringMessages.createUser(), new ClickHandler() { @Override public void onClick(ClickEvent event) { @@ -57,7 +65,7 @@ public class UserManagementPanel extends DockPanel { final Button deleteButton = new Button(stringMessages.deleteUser(), new ClickHandler() { @Override public void onClick(ClickEvent event) { - final UserDTO userToDelete = singleSelectionModel.getSelectedObject(); + final UserDTO userToDelete = refreshableSingleSelectionModel.getSelectedObject(); final String username = userToDelete.getName(); if (Window.confirm(stringMessages.doYouReallyWantToDeleteUser(username))) { userManagementService.deleteUser(username, new AsyncCallback() { @@ -78,12 +86,12 @@ public class UserManagementPanel extends DockPanel { } }); buttonPanel.add(deleteButton); - deleteButton.setEnabled(singleSelectionModel.getSelectedObject() != null); + deleteButton.setEnabled(refreshableSingleSelectionModel.getSelectedObject() != null); final UserList userList = new UserList(); - userList.setSelectionModel(singleSelectionModel); + userList.setSelectionModel(refreshableSingleSelectionModel); TextBox filterBox = new TextBox(); userListDataProvider = new UserListDataProvider(userManagementService, filterBox); - final UserDetailsView userDetailsView = new UserDetailsView(userService, singleSelectionModel.getSelectedObject(), stringMessages, userListDataProvider, permissionsForRoleProvider); + final UserDetailsView userDetailsView = new UserDetailsView(userService, refreshableSingleSelectionModel.getSelectedObject(), stringMessages, userListDataProvider, permissionsForRoleProvider); add(userDetailsView, DockPanel.CENTER); userDetailsView.addUserChangeEventHandler(new UserChangeEventHandler() { @Override @@ -91,11 +99,11 @@ public class UserManagementPanel extends DockPanel { userListDataProvider.updateDisplays(); } }); - singleSelectionModel.addSelectionChangeHandler(new Handler() { + refreshableSingleSelectionModel.addSelectionChangeHandler(new Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { - userDetailsView.updateUser(singleSelectionModel.getSelectedObject(), userManagementService); - deleteButton.setEnabled(singleSelectionModel.getSelectedObject() != null); + userDetailsView.updateUser(refreshableSingleSelectionModel.getSelectedObject(), userManagementService); + deleteButton.setEnabled(refreshableSingleSelectionModel.getSelectedObject() != null); } }); userList.setPageSize(20); @@ -127,7 +135,7 @@ public class UserManagementPanel extends DockPanel { if (user != null) { for (UserDTO u : userList.getVisibleItems()) { if (u.getName().equals(user.getName())) { - singleSelectionModel.setSelected(user, true); + refreshableSingleSelectionModel.setSelected(user, true); } } } From 04052064ef37df2f77c51a040592fdf89ee772b6 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 7 Dec 2015 16:16:25 +0100 Subject: [PATCH 113/306] newSelectionModel: made refreshSelectionModel private --- .../AbstractLeaderboardConfigPanel.java | 1 - .../AbstractTrackedRacesListComposite.java | 1 - .../adminconsole/CompetitorTableWrapper.java | 2 - .../ControlPointTableWrapper.java | 1 - .../DeviceConfigurationListComposite.java | 1 - .../ui/adminconsole/EventListComposite.java | 1 - .../LeaderboardGroupConfigPanel.java | 3 - .../gwt/ui/adminconsole/MarkTableWrapper.java | 1 - .../gwt/ui/adminconsole/MediaPanel.java | 1 - .../ui/adminconsole/RegattaListComposite.java | 1 - .../RemoteServerInstancesManagementPanel.java | 1 - .../ui/adminconsole/WaypointTableWrapper.java | 1 - .../RefreshableMultiSelectionModel.java | 76 +------------------ .../celltable/RefreshableSelectionModel.java | 8 +- .../RefreshableSingleSelectionModel.java | 67 +--------------- 15 files changed, 9 insertions(+), 157 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index f071f525547..fc3ffe3b8b6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -312,7 +312,6 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement availableLeaderboardList.clear(); Util.addAll(leaderboards, availableLeaderboardList); filterLeaderboardPanel.updateAll(availableLeaderboardList); // also maintains the filtered leaderboardList - refreshableLeaderboardSelectionModel.refreshSelectionModel(leaderboards); leaderboardRaceColumnSelectionChanged(); } 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 945ad3086bf..5ec00888c20 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 @@ -481,7 +481,6 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl } allRaces = newAllRaces; filterablePanelRaces.updateAll(allRaces); - refreshableSelectionModel.refreshSelectionModel(allRaces); } /** diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java index 462b37f4718..485ec21b120 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/CompetitorTableWrapper.java @@ -252,7 +252,6 @@ public class CompetitorTableWrapper result) { refreshCompetitorList(result); - ((RefreshableSelectionModel) getSelectionModel()).refreshSelectionModel(result); if (callback != null) callback.onSuccess(result); } }); @@ -268,7 +267,6 @@ public class CompetitorTableWrapper result) { getFilteredCompetitors(result); refreshCompetitorList(result); - ((RefreshableSelectionModel) getSelectionModel()).refreshSelectionModel(result); if (callback != null) callback.onSuccess(result); } }); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java index 7b9d8d400f9..e4f01fb7aa8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java @@ -46,6 +46,5 @@ public class ControlPointTableWrapper controllPoints) { super.refresh(controllPoints); - ((RefreshableSelectionModel) getSelectionModel()).refreshSelectionModel(controllPoints); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java index b40f5259bf2..e786dde412b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java @@ -79,7 +79,6 @@ public class DeviceConfigurationListComposite extends Composite { ColumnSortEvent.fire(configurationTable, configurationTable.getColumnSortList()); noConfigurationsLabel.setVisible(false); - refreshableConfigurationSelectionModel.refreshSelectionModel(result); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 61df37f985d..014e1d5dfc8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -595,7 +595,6 @@ public class EventListComposite extends Composite implements EventsRefresher, Le eventTable.setVisible(true); noEventsLabel.setVisible(false); } - refreshableEventSelectionModel.refreshSelectionModel(events); allEvents.clear(); allEvents.addAll(events); filterTextbox.updateAll(allEvents); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java index b1b35a53d9c..3ec43e1a9f8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java @@ -602,8 +602,6 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements Util.addAll(groups, availableLeaderboardGroups); } groupsFilterablePanel.updateAll(availableLeaderboardGroups); - - refreshableGroupsSelectionModel.refreshSelectionModel(groups); } @Override @@ -822,7 +820,6 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements descriptionTextArea.setText(selectedGroup.description); setDescriptionEditable(false); - refreshableGroupDetailsSelectionModel.refreshSelectionModel(availableLeaderboards); groupDetailsProvider.getList().clear(); groupDetailsProvider.getList().addAll(selectedGroup.leaderboards); groupDetailsProvider.refresh(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java index a52565138de..44ac89c5f6b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MarkTableWrapper.java @@ -99,6 +99,5 @@ public class MarkTableWrapper> exte return o1.getName().compareTo(o2.getName()); } }); - ((RefreshableSelectionModel) getSelectionModel()).refreshSelectionModel(marks); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java index ee9b916908c..61507265892 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java @@ -145,7 +145,6 @@ public class MediaPanel extends FlowPanel { allMediaTracks.addAll(mediaTrackListDataProvider.getList()); filterableMediaTracks.updateAll(allMediaTracks); mediaTrackListDataProvider.refresh(); - refreshableSelectionModel.refreshSelectionModel(allMediaTracks); } }); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index 1d3752a6f2f..95e75efe996 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -352,7 +352,6 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer Util.addAll(regattas, newAllRegattas); allRegattas = newAllRegattas; filterablePanelRegattas.updateAll(allRegattas); - refreshableRegattaMultiSelectionModel.refreshSelectionModel(regattas); } public List getAllRegattas() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java index 29df3c012a5..3b915c4afd6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java @@ -183,7 +183,6 @@ public class RemoteServerInstancesManagementPanel extends SimplePanel { @Override public void onSuccess(List result) { filteredServerTablePanel.updateAll(result); - refreshableServerSelectionModel.refreshSelectionModel(result); } }); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java index dbd7cd7093b..cc92541ff5c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java @@ -50,6 +50,5 @@ public class WaypointTableWrapper wayPoints) { super.refresh(wayPoints); - ((RefreshableSelectionModel) getSelectionModel()).refreshSelectionModel(wayPoints); } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 3f33444935b..f0c00bc3c6c 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -1,7 +1,5 @@ package com.sap.sse.gwt.client.celltable; -import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Set; @@ -31,7 +29,6 @@ import com.google.gwt.view.client.SelectionModel; public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; - private final List elements; private boolean dontcheckSelectionState = false; /** @@ -41,7 +38,6 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel public RefreshableMultiSelectionModel(EntityIdentityComparator comp) { super(); this.comp = comp; - elements = new ArrayList<>(); } /** @@ -53,7 +49,6 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel public RefreshableMultiSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { super(keyProvider); this.comp = comp; - elements = new ArrayList<>(); } @Override @@ -89,11 +84,12 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel } /** - * refreshes the {@link RefreshableMultiSelectionModel SelectionModel} with all elements of the {@link ListDataProvider} + * refreshes the {@link RefreshableMultiSelectionModel SelectionModel} with all elements of the + * {@link ListDataProvider} *

    * FIXME change to private */ - public void refreshSelectionModel(Iterable newObjects) { + private void refreshSelectionModel(Iterable newObjects) { dontcheckSelectionState = true; final Set selectedSet = getSelectedSet(); final boolean isEmpty = selectedSet.isEmpty(); @@ -118,73 +114,9 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel dontcheckSelectionState = false; } - /** - * refreshes the {@link RefreshableMultiSelectionModel SelectionModel}-Part from start to - * elements.size() - * - * @param start - * @param newObjects - */ - @Override - public void refreshSelectionModel(int start, List newObjects) { - dontcheckSelectionState = true; - List oldElements = elements.subList(start, elements.size()); - Set selectedElements = new HashSet(); - for (T it : oldElements) { - if (isSelected(it)) { - selectedElements.add(it); - setSelected(it, false); - } - } - if (!selectedElements.isEmpty()) { - for (T newElement : newObjects) { - if (comp != null) { - for (T selected : selectedElements) { - if (comp.representSameEntity(selected, newElement)) { - setSelected(newElement, true); - break; - } - } - } else { - if (selectedElements.contains(newElement)) { - setSelected(newElement, true); - } - } - } - SelectionChangeEvent.fire(this); - } - isSelected(newObjects.iterator().next()); // Triggers a clear of unselected elements in superclass - dontcheckSelectionState = false; - } - - @SuppressWarnings("unchecked") @Override public void setRowData(int start, List values) { - if (!values.equals(elements.subList(start, elements.size()))) { - // refresh the selectionModel - if (start == 0 && (values.size() > 1 || values.size()==0)) { - refreshSelectionModel((Iterable) values); - } else { - refreshSelectionModel(start, (List) values); - } - // refresh the element list - // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider - if (values.isEmpty()) { - elements.clear(); - } else if (elements.isEmpty()) { - elements.addAll(values); - } else if (values.size() == 1) { - elements.set(start, values.get(0)); - } else if (start == 0) { - elements.clear(); - elements.addAll(values); - } else { - for (int i = start; i < elements.size(); i++) { - elements.remove(i); - } - elements.addAll(values); - } - } + //TODO /FIME refresh selectionModel with all data of ListDataProvider } @Override diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java index 6cc85922787..6deb0503e85 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java @@ -1,7 +1,5 @@ package com.sap.sse.gwt.client.celltable; -import java.util.List; - import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.SetSelectionModel; import com.google.gwt.view.client.SingleSelectionModel; @@ -39,8 +37,6 @@ public interface RefreshableSelectionModel extends SetSelectionModel { * {@link SelectionChangeEvent.Handler#onSelectionChange(SelectionChangeEvent) onSelectionChangedEvent} using * {@link AbstractSelectionModel#fireEvent(com.google.gwt.event.shared.GwtEvent)}. */ - // consider deleting this method and switch to private in sub classes - public void refreshSelectionModel(Iterable newObjects); - // consider deleting this method and switch to private in sub classes - public void refreshSelectionModel(int start, List newObjects); + // TODO / FIXME delete this method and describe interaction + // public void refreshSelectionModel(Iterable newObjects); } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index b2599062ca4..282515a1980 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -1,6 +1,5 @@ package com.sap.sse.gwt.client.celltable; -import java.util.ArrayList; import java.util.List; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.view.client.HasData; @@ -19,7 +18,6 @@ import com.google.gwt.view.client.RangeChangeEvent.Handler; public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; - private final List elements; private boolean dontcheckSelectionState = false; /** @@ -29,7 +27,6 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel public RefreshableSingleSelectionModel(EntityIdentityComparator comp) { super(); this.comp=comp; - elements = new ArrayList<>(); } /** @@ -41,7 +38,6 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel public RefreshableSingleSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { super(keyProvider); this.comp =comp; - elements = new ArrayList<>(); } @Override @@ -62,8 +58,8 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel } } } - @Override - public void refreshSelectionModel(Iterable newObjects) { + + private void refreshSelectionModel(Iterable newObjects) { dontcheckSelectionState = true; final T selected = getSelectedObject(); if (selected != null) { @@ -79,67 +75,10 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel SelectionChangeEvent.fire(this); dontcheckSelectionState = false; } - - /** - * refreshes the {@link RefreshableMultiSelectionModel SelectionModel}-Part from start to - * elements.size() - * - * @param start - * @param newObjects - */ - @Override - public void refreshSelectionModel(int start, List newObjects) { - dontcheckSelectionState = true; - T selectedElement = getSelectedObject(); - if (selectedElement != null) { - final int index = elements.indexOf(selectedElement); - if (index < 0 || index < start) { - return; - } - setSelected(selectedElement, false); - if (comp != null) { - for (T newElement : newObjects) { - if (comp.representSameEntity(selectedElement, newElement)) { - setSelected(newElement, true); - break; - } - } - } else if (newObjects.contains(selectedElement)) { - setSelected(newObjects.get(newObjects.indexOf(selectedElement)), true); - } - SelectionChangeEvent.fire(this); - } - dontcheckSelectionState = false; - } - @SuppressWarnings("unchecked") @Override public void setRowData(int start, List values) { - if (!values.equals(elements.subList(start, elements.size()))) { - // refresh the selectionModel - if (start == 0 && (values.size() > 1 || values.size() == 0)) { - refreshSelectionModel((Iterable) values); - } else { - refreshSelectionModel(start, (List) values); - } - // refresh the element list - // TODO discuss with Axel if this depends to much of the implementation of ListDataProvider - if (values.isEmpty()) { - elements.clear(); - } else if (elements.isEmpty()) { - elements.addAll(values); - } else if (values.size() == 1) { - elements.set(start, values.get(0)); - } else if (start == 0) { - elements.clear(); - elements.addAll(values); - } else { - for (int i = start; i < elements.size(); i++) { - elements.remove(i); - } - elements.addAll(values); - } - } + //TODO / FIXME update with all elements of ListDataProvider } @Override From 40282c6a3c954ad2b429fe476852811feef47fd7 Mon Sep 17 00:00:00 2001 From: fmittag Date: Mon, 7 Dec 2015 16:59:30 +0100 Subject: [PATCH 114/306] Merge remote-tracking branch 'origin/master' into bug2516 --- configuration/syncEC2ElbLogs | 2 + configuration/unique_ips_per_referrer | 4 +- ...s_per_referrer_generate_month_results_only | 16 + ...que_ips_per_referrer_generate_results_only | 26 + .../com/sap/sailing/autoload/TracTrac.java | 4 +- java/com.sap.sailing.dashboards.gwt/pom.xml | 13 - java/com.sap.sailing.domain.common/pom.xml | 80 +- .../domain/common/AbstractSpeedImpl.java | 2 +- .../common/dto/AbstractLeaderboardDTO.java | 863 +-- .../domain/common/dto/BasicRaceDTO.java | 112 + .../domain/common/dto/LeaderboardDTO.java | 3 +- .../dto/MetaLeaderboardRaceColumnDTO.java | 49 + .../common/dto/RaceColumnDTOFactory.java | 10 +- .../sailing/domain/common/dto/RaceDTO.java | 85 +- .../META-INF/MANIFEST.MF | 3 +- .../impl/DomainObjectFactoryImpl.java | 8 +- .../impl/MongoObjectFactoryImpl.java | 3066 ++++---- .../pom.xml | 78 +- .../sap/sailing/domain/base/EventBase.java | 196 +- .../domain/base/impl/EventBaseImpl.java | 12 +- .../resources/IncrementalLeaderboardDTO.ser | Bin 223080 -> 271138 bytes .../test/MultipleClassesInRegattaTest.java | 8 +- ...nicodeCharactersInCompetitorNamesTest.java | 2 +- .../domain/tractracadapter/DomainFactory.java | 24 +- .../tractracadapter/TracTracAdapter.java | 7 +- .../impl/DomainFactoryImpl.java | 13 +- ...aceTrackingConnectivityParametersImpl.java | 11 +- .../impl/RawPositionReceiver.java | 13 +- .../tractracadapter/impl/Simulator.java | 54 +- .../impl/TracTracAdapterImpl.java | 7 +- .../impl/TracTracRaceTrackerImpl.java | 25 +- .../com/sap/sailing/domain/base/Course.java | 2 + .../sailing/domain/base/impl/CourseImpl.java | 5 + .../impl/AbstractSimpleLeaderboardImpl.java | 3844 +++++----- .../impl/FlexibleLeaderboardImpl.java | 678 +- .../impl/RegattaLeaderboardImpl.java | 240 +- .../meta/FlexibleMetaLeaderboard.java | 134 +- .../meta/LeaderboardGroupMetaLeaderboard.java | 190 +- .../domain/tracking/impl/TrackedRaceImpl.java | 103 +- .../raceanalysis.product | 1 + .../ui/test/GwtTestCaseColumnToggling.java | 2 +- .../gwt/ui/test/LeaderboardPanelMock.java | 2 +- .../gwt/ui/test/TestColumnSwapping.java | 324 +- java/com.sap.sailing.gwt.ui/Leaderboard.css | 4 +- .../META-INF/MANIFEST.MF | 5 +- java/com.sap.sailing.gwt.ui/RaceBoard.css | 47 +- .../SailingGWT sdm.launch | 4 +- java/com.sap.sailing.gwt.ui/SailingGWT.launch | 4 +- java/com.sap.sailing.gwt.ui/pom.xml | 13 - .../place/player/AutoPlayController.java | 8 +- .../event/GetEventViewAction.java | 2 +- .../GetEventOverviewStageAction.java | 2 +- .../news/RaceCompetitorNewsEntryDTO.java | 2 + .../fakeseries/GetEventSeriesViewAction.java | 2 +- .../media/GetMediaForEventAction.java | 4 +- .../start/GetStartViewAction.java | 4 +- .../home/desktop/utils/EventParamUtils.java | 2 +- .../places/event/AbstractEventActivity.java | 2 - .../gwt/home/shared/ExperimentalFeatures.java | 2 +- .../client/RegattaOverviewPanel.java | 2 +- .../sailing/gwt/server/HomeServiceUtil.java | 6 +- .../gwt/ui/adminconsole/AddWindFixDialog.java | 8 +- .../RaceLogCourseManagementWidget.java | 14 +- .../SwissTimingEventManagementPanel.java | 2 +- .../TracTracEventManagementPanel.java | 71 +- .../sailing/gwt/ui/client/RaceTimePanel.java | 4 +- .../sailing/gwt/ui/client/SailingService.java | 3 +- .../gwt/ui/client/SailingServiceAsync.java | 9 +- .../sailing/gwt/ui/client/StringMessages.java | 10 +- .../gwt/ui/client/StringMessages.properties | 10 +- .../ui/client/StringMessages_de.properties | 12 +- .../ui/client/StringMessages_ru.properties | 10 +- .../ui/client/StringMessages_zh.properties | 12 +- .../sap/sailing/gwt/ui/client/TimePanel.java | 12 +- .../ui/client/media/GalleryImageHolder.java | 4 +- .../gwt/ui/client/media/VideoThumbnail.java | 4 +- .../ui/client/shared/racemap/Hoverline.java | 57 + .../gwt/ui/client/shared/racemap/RaceMap.java | 5237 ++++++------- .../shared/racemap/RaceMapSettings.java | 22 + .../RaceMapSettingsDialogComponent.java | 24 +- .../gwt/ui/leaderboard/CompetitorFilter.css | 5 +- .../ui/leaderboard/LeaderboardEntryPoint.java | 2 +- .../gwt/ui/leaderboard/LeaderboardPanel.java | 49 +- .../LeaderboardSettingsFactory.java | 24 +- .../LeaderboardUrlConfigurationDialog.java | 2 +- .../ui/leaderboard/MultiLeaderboardPanel.java | 2 +- .../EditableLeaderboardPanel.java | 2 +- .../EmbeddedMapAndWindChartEntryPoint.java | 3 +- .../gwt/ui/raceboard/RaceBoardEntryPoint.java | 402 +- .../gwt/ui/raceboard/RaceBoardPanel.java | 920 +-- .../gwt/ui/raceboard/ResizableFlowPanel.java | 23 + .../raceboard/SideBySideComponentViewer.java | 639 +- .../ui/raceboard/TouchSplitLayoutPanel.java | 22 +- .../gwt/ui/server/SailingServiceImpl.java | 83 +- .../gwt/ui/simulator/SimulatorMainPanel.java | 4 +- .../home/solutions/SailingAnalyticsNotes.html | 20 + .../META-INF/MANIFEST.MF | 3 +- ...estStoringAndLoadingEventsAndRegattas.java | 1672 ++--- .../data/impl/GPSFixWithPolarContext.java | 2 +- .../META-INF/MANIFEST.MF | 3 +- .../pom.xml | 50 +- .../impl/EventBaseJsonDeserializer.java | 8 +- .../impl/EventBaseJsonSerializer.java | 4 +- .../META-INF/MANIFEST.MF | 3 +- .../test/EventDataJsonSerializerTest.java | 4 +- ...JsonSerializerWithImagesAndVideosTest.java | 8 +- ...tDataJsonSerializerWithNullValuesTest.java | 4 +- ...NullStartAndEndDataJsonSerializerTest.java | 4 +- ...BoatCompetitorMetadataReplicationTest.java | 2 +- .../test/TrackRaceReplicationTest.java | 2 +- .../server/test/MasterDataImportTest.java | 4 +- .../server/test/SearchServiceTest.java | 4 +- .../sailing/server/RacingEventService.java | 1246 +-- .../server/impl/RacingEventServiceImpl.java | 6688 ++++++++--------- .../CreateEvent.java | 140 +- .../UpdateEvent.java | 122 +- java/com.sap.sse.common/pom.xml | 50 +- .../pom.xml | 56 +- .../sse/gwt/client/controls/IntegerBox.java | 3 + .../gwt/client/controls/slider/SliderBar.java | 2512 +++---- .../client/controls/slider/TimeSlider.java | 24 +- .../controls/slider/TimeTicksCalculator.java | 493 +- .../sse/gwt/resources/css/CommonControls.css | 10 +- java/com.sap.sse.security.common/pom.xml | 64 +- .../META-INF/MANIFEST.MF | 2 + .../sse/shared}/media/ImageDescriptor.java | 2 +- .../sse/shared}/media/MediaDescriptor.java | 3 +- .../com/sap/sse/shared}/media/MediaUtils.java | 3 +- .../sse/shared}/media/VideoDescriptor.java | 2 +- .../com/sap/sse/shared}/media/WithImages.java | 3 +- .../com/sap/sse/shared}/media/WithMedia.java | 2 +- .../com/sap/sse/shared}/media/WithVideos.java | 3 +- .../media/impl}/AbstractMediaDescriptor.java | 4 +- .../media/impl}/ImageDescriptorImpl.java | 4 +- .../media/impl}/VideoDescriptorImpl.java | 4 +- .../src/com/sap/sse/ServerInfo.java | 10 +- .../src/com/sap/sse/util/IdentityWrapper.java | 34 + java/org.json.simple/pom.xml | 80 +- java/pom.xml | 22 +- java/target/configuration/jetty/etc/jetty.xml | 2 +- mobile/com.sap.sailing.android.shared/pom.xml | 36 +- .../com.sap.sailing.racecommittee.app/pom.xml | 2 +- mobile/pom.xml | 196 +- pom.xml | 116 +- wiki/Log-File-Analysis.md | 86 +- wiki/creating-ec2-image-from-scratch.md | 3 +- wiki/onboarding.md | 10 +- 147 files changed, 16378 insertions(+), 15599 deletions(-) create mode 100755 configuration/syncEC2ElbLogs create mode 100755 configuration/unique_ips_per_referrer_generate_month_results_only create mode 100755 configuration/unique_ips_per_referrer_generate_results_only create mode 100644 java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java create mode 100644 java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/Hoverline.java create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/ResizableFlowPanel.java rename java/{com.sap.sse.common/src/com/sap/sse/common => com.sap.sse.shared.android/src/com/sap/sse/shared}/media/ImageDescriptor.java (90%) rename java/{com.sap.sse.common/src/com/sap/sse/common => com.sap.sse.shared.android/src/com/sap/sse/shared}/media/MediaDescriptor.java (91%) rename java/{com.sap.sse.common/src/com/sap/sse/common => com.sap.sse.shared.android/src/com/sap/sse/shared}/media/MediaUtils.java (98%) rename java/{com.sap.sse.common/src/com/sap/sse/common => com.sap.sse.shared.android/src/com/sap/sse/shared}/media/VideoDescriptor.java (87%) rename java/{com.sap.sse.common/src/com/sap/sse/common => com.sap.sse.shared.android/src/com/sap/sse/shared}/media/WithImages.java (95%) rename java/{com.sap.sse.common/src/com/sap/sse/common => com.sap.sse.shared.android/src/com/sap/sse/shared}/media/WithMedia.java (91%) rename java/{com.sap.sse.common/src/com/sap/sse/common => com.sap.sse.shared.android/src/com/sap/sse/shared}/media/WithVideos.java (95%) rename java/{com.sap.sse.common/src/com/sap/sse/common/media => com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl}/AbstractMediaDescriptor.java (95%) rename java/{com.sap.sse.common/src/com/sap/sse/common/media => com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl}/ImageDescriptorImpl.java (91%) rename java/{com.sap.sse.common/src/com/sap/sse/common/media => com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl}/VideoDescriptorImpl.java (89%) create mode 100644 java/com.sap.sse/src/com/sap/sse/util/IdentityWrapper.java diff --git a/configuration/syncEC2ElbLogs b/configuration/syncEC2ElbLogs new file mode 100755 index 00000000000..25affdba085 --- /dev/null +++ b/configuration/syncEC2ElbLogs @@ -0,0 +1,2 @@ +#!/bin/sh +/usr/bin/s3cmd -c /root/.s3cfg sync s3://sapsailing-access-logs/elb-access-logs/ /var/log/old/elb-access-logs/ >/var/log/last-elb-access-logs-sync.out 2>/var/log/last-elb-access-logs-sync.err diff --git a/configuration/unique_ips_per_referrer b/configuration/unique_ips_per_referrer index d06f2201d0a..518f7f27509 100755 --- a/configuration/unique_ips_per_referrer +++ b/configuration/unique_ips_per_referrer @@ -16,6 +16,8 @@ mkdir -p "$STATS" # A sample line: # 505Worlds2012.sapsailing.com 66.249.73.53 - - [12/Jan/2014:03:33:11 +0000] "GET /robots.txt HTTP/1.1" 404 238 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" +echo Starting "$0" at `date` on file set "$*" + for i in $*; do grep -q "^$i\$" $VISITED_FILES if [ "$?" = "0" ]; then @@ -50,4 +52,4 @@ for month in `cat $MONTHS`; do done | tee $STATS/unique-ips-days-useragents-per-event-$month echo "Wrote per-month results to $STATS/unique-ips-days-useragents-per-event-$month" done -echo "Done." +echo "Done at `date`." diff --git a/configuration/unique_ips_per_referrer_generate_month_results_only b/configuration/unique_ips_per_referrer_generate_month_results_only new file mode 100755 index 00000000000..dcea8f54211 --- /dev/null +++ b/configuration/unique_ips_per_referrer_generate_month_results_only @@ -0,0 +1,16 @@ +#!/bin/bash + +CACHE=/var/log/old/cache/unique-ips-per-referrer +VISITED_FILES=$CACHE/visited +STATS=$CACHE/stats +MONTHS=$STATS/months + +for month in `cat $MONTHS`; do + for i in ${STATS}/*.ips; do + echo -n "`basename $i .ips` " + cat $i | grep "^[^ ]* [0-9]*/$month .*" | sort -u | wc | awk '{ print $1; }' + # TODO consider producing an AWStats configuration file for each virtual host name and splitting all new logs into virtual host name-specific log files + done | tee $STATS/unique-ips-days-useragents-per-event-`echo $month | tr / -` + echo "Wrote per-month results to $STATS/unique-ips-days-useragents-per-event-$month" +done +echo "Done." diff --git a/configuration/unique_ips_per_referrer_generate_results_only b/configuration/unique_ips_per_referrer_generate_results_only new file mode 100755 index 00000000000..23e316484ce --- /dev/null +++ b/configuration/unique_ips_per_referrer_generate_results_only @@ -0,0 +1,26 @@ +#!/bin/bash + +CACHE=/var/log/old/cache/unique-ips-per-referrer +VISITED_FILES=$CACHE/visited +STATS=$CACHE/stats +MONTHS=$STATS/months + +for i in ${STATS}/*.ips; do + echo -n "`basename $i .ips` " + cat $i | sort -u | wc | awk '{ print $1; }' + # TODO consider producing an AWStats configuration file for each virtual host name and splitting all new logs into virtual host name-specific log files +done | tee $STATS/unique-ips-days-useragents-per-event +echo "Wrote total results to $STATS/unique-ips-days-useragents-per-event" + + +# Now filter and group by month +cat ${STATS}/*.ips | awk '{ print $2; }' | sed -e 's/^[0-9]*\///' | sort -u >$MONTHS +for month in `cat $MONTHS`; do + for i in ${STATS}/*.ips; do + echo -n "`basename $i .ips` " + cat $i | grep "^[^ ]* [0-9]*/$month .*" | sort -u | wc | awk '{ print $1; }' + # TODO consider producing an AWStats configuration file for each virtual host name and splitting all new logs into virtual host name-specific log files + done | tee $STATS/unique-ips-days-useragents-per-event-`echo $month | tr / -` + echo "Wrote per-month results to $STATS/unique-ips-days-useragents-per-event-$month" +done +echo "Done." diff --git a/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java b/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java index 3ecf17a7c01..1df916f6e48 100644 --- a/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java +++ b/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java @@ -94,8 +94,8 @@ public class TracTrac { new MillisecondsTimePoint(record.getTrackingStartTime().asMillis()), new MillisecondsTimePoint(record.getTrackingEndTime().asMillis()), raceLogStore, regattaLogStore, - RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, /* simulateWithStartTimeNow */ - false, /*useTracTracMarkPassings*/ false, + RaceTracker.TIMEOUT_FOR_RECEIVING_RACE_DEFINITION_IN_MILLISECONDS, /* offsetToStartTimeOfSimulatedRace */ + null, /*useTracTracMarkPassings*/ false, /* tracTracUsername */"", /* tracTracPassword */"", record.getRaceStatus(), record.getRaceVisibility()); } else { diff --git a/java/com.sap.sailing.dashboards.gwt/pom.xml b/java/com.sap.sailing.dashboards.gwt/pom.xml index 9bc75ff31f7..ec9512cb84d 100644 --- a/java/com.sap.sailing.dashboards.gwt/pom.xml +++ b/java/com.sap.sailing.dashboards.gwt/pom.xml @@ -11,19 +11,6 @@ com.sap.sailing.dashboards.gwt eclipse-plugin - - - sonatype - http://oss.sonatype.org/content/repositories/snapshots - - true - - - false - - - - javax.validation diff --git a/java/com.sap.sailing.domain.common/pom.xml b/java/com.sap.sailing.domain.common/pom.xml index 031a6daf0f6..a1ae29ff16f 100755 --- a/java/com.sap.sailing.domain.common/pom.xml +++ b/java/com.sap.sailing.domain.common/pom.xml @@ -1,40 +1,40 @@ - - - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - com.sap.sailing.domain.common - eclipse-plugin - - - - - org.eclipse.tycho - tycho-compiler-plugin - ${tycho-version} - - 1.7 - 1.7 - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - - - + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.domain.common + eclipse-plugin + + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + 1.7 + 1.7 + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractSpeedImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractSpeedImpl.java index 90b3c7e5116..4b38e380d31 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractSpeedImpl.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/AbstractSpeedImpl.java @@ -21,7 +21,7 @@ public abstract class AbstractSpeedImpl implements Speed { @Override public Duration getDuration(Distance distance) { - return new MillisecondsDurationImpl((long) (1000 * distance.getMeters() / getMetersPerSecond())); + return distance == null ? null : new MillisecondsDurationImpl((long) (1000 * distance.getMeters() / getMetersPerSecond())); } @Override diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java index 8a9fa9017a9..ff197288789 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/AbstractLeaderboardDTO.java @@ -1,430 +1,433 @@ -package com.sap.sailing.domain.common.dto; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import com.sap.sailing.domain.common.LeaderboardType; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sse.common.Util; - -public abstract class AbstractLeaderboardDTO implements Serializable { - private static final long serialVersionUID = -205106531931903527L; - - public String name; - - private List races; - public Map competitorDisplayNames; - public Map rows; - public boolean hasCarriedPoints; - public int[] discardThresholds; - - /** - * Set to the non-null regatta name if this DTO represents a RegattaLeaderboard. - */ - public String regattaName; - public String displayName; - public UUID defaultCourseAreaId; - public String defaultCourseAreaName; - public ScoringSchemeType scoringScheme; - public LeaderboardType type; - - private Long delayToLiveInMillisForLatestRace; - - public AbstractLeaderboardDTO() { - races = new ArrayList(); - } - - public Set getBoatClasses() { - Set result = new HashSet(); - if(rows != null) { - for (CompetitorDTO competitor : rows.keySet()) { - result.add(competitor.getBoatClass()); - } - } - return result; - } - - public String getDisplayName() { - return displayName; - } - - public String getDisplayName(CompetitorDTO competitor) { - if (competitorDisplayNames == null || competitorDisplayNames.get(competitor) == null) { - return competitor.getName(); - } else { - return competitorDisplayNames.get(competitor); - } - } - - /** - * If the race column whose name is specified in raceColumnName has at least one competitor who has valid - * {@link LeaderboardEntryDTO#legDetails} for that race column, the maximum number of entries of all such competitors in - * the leg details is returned, telling the number of legs that the race column shall display. Otherwise, -1 is returned. - * If you specify an non null preselectedRace the leg count is calculated only for that race for the column - * holding that race. All other columns will be unaffected by preselectedRace and will return their regular - * leg count. Note that this may lead to a situation where some competitors may have sailed more legs than others because - * their fleet association may have varied across races.

    - * - * See also bug 2604 and bug 2035. - */ - public int getLegCount(String raceColumnName, RaceIdentifier preselectedRace) { - int result = -1; - final boolean preselectedRaceIsInRaceColumn = isRaceInColumn(preselectedRace, raceColumnName); - for (LeaderboardRowDTO row : rows.values()) { - LeaderboardEntryDTO leaderboardEntryDTO = row.fieldsByRaceColumnName.get(raceColumnName); - if (leaderboardEntryDTO != null && leaderboardEntryDTO.legDetails != null - // when no race is pre-selected, always use the actual leg count; - // otherwise, if the pre-selected race is in the column identified by - // raceColumnName, use the entry only if it's in the pre-selected race; - // otherwise (a race is pre-selected but a different column's leg count - // is requested), use the regular leg count as is. - && (preselectedRace == null || !preselectedRaceIsInRaceColumn || preselectedRace.equals(leaderboardEntryDTO.race))) { - result = Math.max(result, leaderboardEntryDTO.legDetails.size()); - } - } - return result; - } - - private boolean isRaceInColumn(RaceIdentifier preselectedRace, String raceColumnName) { - RaceColumnDTO raceColumn = getRaceColumnByName(raceColumnName); - return raceColumn.containsRace(preselectedRace); - } - - /** - * Tells if the competitor scored (and therefore presumably participated) in a medal race represented - * in this leaderboard. - */ - public boolean scoredInMedalRace(CompetitorDTO competitor) { - LeaderboardRowDTO row = rows.get(competitor); - for (RaceColumnDTO race : races) { - if (race.isMedalRace() && row.fieldsByRaceColumnName.get(race.getRaceColumnName()).totalPoints > 0) { - return true; - } - } - return false; - } - - public Double getNetPoints(CompetitorDTO competitor, String nameOfLastRaceSoFar) { - Double netPoints = null; - LeaderboardRowDTO row = rows.get(competitor); - if (row != null) { - LeaderboardEntryDTO field = row.fieldsByRaceColumnName.get(nameOfLastRaceSoFar); - if (field != null && field.netPoints != null) { - netPoints = field.netPoints; - } - } - return netPoints; - } - - public boolean raceIsTracked(String raceColumnName) { - for (RaceColumnDTO race : races) { - if (race.getRaceColumnName().equals(raceColumnName)) { - for (FleetDTO fleet : race.getFleets()) { - if (race.isTrackedRace(fleet)) { - return true; - } - } - } - } - return false; - - } - - public boolean raceIsMedalRace(String raceColumnName) { - return getRaceColumnByName(raceColumnName).isMedalRace(); - } - - /** - * If the {@link RaceColumnDTO} by the name raceColumnName doesn't exist yet within this leaderboard - * DTO, it is created, setting is {@link RaceColumnDTO#isValidInTotalScore()} to true. This method - * ensures that a fleet named fleetName is present. If it's not present yet, it's added to the race - * column's fleet name list. The trackedRaceIdentifier and race are associated with the - * column for the fleet identified by fleetName. - * - * @param explicitFactor - * factor by which to multiply the race column's points for the overall score; if null, the - * default will be determined by whether or not the column is marked as medal race - * @param effectiveFactor - * is what you get when you call {@link RaceColumn#getFactor()} on the race column that the resulting - * {@link RaceColumnDTO} represents - * @param regattaName - * must not be null if seriesName is not null; specified - * separately from trackedRaceIdentifier because a column may belong to regatta/series - * despite not having a tracked race associated - * @param seriesName - * if null, this method will produce a {@link RaceColumnDTO}, otherwise a - * {@link RaceColumnInSeriesDTO} - * @param fleetDTO - * must not be null - */ - public RaceColumnDTO addRace(String raceColumnName, Double explicitFactor, double effectiveFactor, - String regattaName, String seriesName, FleetDTO fleetDTO, boolean medalRace, RegattaAndRaceIdentifier trackedRaceIdentifier, RaceDTO race) { - assert fleetDTO != null; - RaceColumnDTO raceColumnDTO = getRaceColumnByName(raceColumnName); - if (raceColumnDTO == null) { - raceColumnDTO = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, medalRace, - explicitFactor, regattaName, seriesName); - races.add(raceColumnDTO); - } - raceColumnDTO.setEffectiveFactor(effectiveFactor); - boolean contains = false; - for (FleetDTO fleet : raceColumnDTO.getFleets()) { - if (fleet.getName().equals(fleetDTO.getName())) { - contains = true; - break; - } - } - if (!contains) { - raceColumnDTO.addFleet(fleetDTO); - } - raceColumnDTO.setRaceIdentifier(fleetDTO, trackedRaceIdentifier); - raceColumnDTO.setRace(fleetDTO, race); - return raceColumnDTO; - } - - public RaceColumnDTO createEmptyRaceColumn(String raceColumnName, boolean medalRace, String regattaName, String seriesName) { - final RaceColumnDTO raceColumn = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, - medalRace, /* explicit factor */ null, regattaName, seriesName); - races.add(raceColumn); - return raceColumn; - } - - public RaceColumnDTO getRaceColumnByName(String raceColumnName) { - for (RaceColumnDTO race : races) { - if (race.getRaceColumnName().equals(raceColumnName)) { - return race; - } - } - return null; - } - - public int getRaceColumnsCount() { - return races.size(); - } - - public int getRacesCount() { - int result = 0; - for (RaceColumnDTO race : getRaceList()) { - result += race.getFleets().size(); - } - return result; - } - - public int getTrackedRacesCount() { - int result = 0; - for (RaceColumnDTO raceColumn : getRaceList()) { - for (FleetDTO fleet : raceColumn.getFleets()) { - RaceDTO race = raceColumn.getRace(fleet); - if(race != null && race.trackedRace != null && race.trackedRace.hasGPSData && race.trackedRace.hasWindData) { - result++; - } - } - } - return result; - } - - public List getRaceList() { - return races; - } - - public void setRaceList(List raceList) { - this.races = raceList; - } - - public boolean raceListContains(String raceColumnName) { - return getRaceColumnByName(raceColumnName) != null; - } - - public void moveRaceUp(String raceColumnName) { - RaceColumnDTO race = getRaceColumnByName(raceColumnName); - int index = races.indexOf(race); - index--; - if (index >= 0) { - races.remove(index + 1); - races.add(index, race); - } - } - - public void moveRaceDown(String raceColumnName) { - RaceColumnDTO race = getRaceColumnByName(raceColumnName); - int index = races.indexOf(race); - if (index != -1) { - index++; - if (index < races.size()) { - races.remove(index - 1); - races.add(index, race); - } - } - } - - public void setIsMedalRace(String raceColumnName, boolean medalRace) { - getRaceColumnByName(raceColumnName).setMedalRace(medalRace); - } - - /** - * @return The earliest start date of the races, or null if no start dates of the races are available. - */ - public Date getStartDate() { - Date leaderboardStart = null; - for (RaceColumnDTO race : getRaceList()) { - for (FleetDTO fleet: race.getFleets()) { - Date raceStart = race.getStartDate(fleet); - if (raceStart != null) { - if (leaderboardStart == null) { - leaderboardStart = new Date(); - } else { - leaderboardStart = leaderboardStart.before(raceStart) ? leaderboardStart : raceStart; - } - } - } - } - return leaderboardStart; - } - - /** - * Takes the {@link PlacemarkOrderDTO} of all races in this leaderboard, if the PlacemarkOrderDTO for the race is - * available, and fills all {@link PlacemarkDTO} in a new PlacemarkOrderDTO.
    - * The order of the races in this leaderboard determine the order of the PlacemarkDTOs in the PlacemarkOrderDTO. - * - * @return The places of this leaderboard in form of a {@link PlacemarkOrderDTO}, or null if the - * {@link PlacemarkOrderDTO places} of no race are available - */ - public PlacemarkOrderDTO getPlaces() { - PlacemarkOrderDTO leaderboardPlaces = null; - for (RaceColumnDTO race : getRaceList()) { - PlacemarkOrderDTO racePlaces = race.getPlaces(); - if (racePlaces != null) { - if (leaderboardPlaces == null) { - leaderboardPlaces = new PlacemarkOrderDTO(); - } - leaderboardPlaces.getPlacemarks().addAll(racePlaces.getPlacemarks()); - } - } - return leaderboardPlaces; - } - - /** - * @return true if the leaderboard contains a race which is live - */ - public boolean hasLiveRace(long serverTimePointAsMillis) { - for (RaceColumnDTO race : getRaceList()) { - for (FleetDTO fleet : race.getFleets()) { - if (race.isLive(fleet, serverTimePointAsMillis)) { - return true; - } - } - } - return false; - } - - public List> getLiveRaces(long serverTimePointAsMillis) { - List> result = new ArrayList>(); - for (RaceColumnDTO race : getRaceList()) { - for (FleetDTO fleet : race.getFleets()) { - if (race.isLive(fleet, serverTimePointAsMillis)) { - result.add(new Util.Pair(race, fleet)); - } - } - } - return result; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((competitorDisplayNames == null) ? 0 : competitorDisplayNames.hashCode()); - result = prime * result + Arrays.hashCode(discardThresholds); - result = prime * result + (hasCarriedPoints ? 1231 : 1237); - result = prime * result + ((name == null) ? 0 : name.hashCode()); - result = prime * result + ((scoringScheme == null) ? 0 : scoringScheme.hashCode()); - if (races == null) { - result = prime * result; - } else { - List raceNames = new ArrayList(); - for (RaceColumnDTO race : races) { - raceNames.add(race.getName()); - } - result = prime * result + raceNames.hashCode(); - } - result = prime * result + ((rows == null) ? 0 : rows.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - AbstractLeaderboardDTO other = (AbstractLeaderboardDTO) obj; - if (competitorDisplayNames == null) { - if (other.competitorDisplayNames != null) - return false; - } else if (!competitorDisplayNames.equals(other.competitorDisplayNames)) - return false; - if (!Arrays.equals(discardThresholds, other.discardThresholds)) - return false; - if (hasCarriedPoints != other.hasCarriedPoints) - return false; - if (type != other.type) - return false; - if (scoringScheme != other.scoringScheme) - return false; - if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; - if (races == null) { - if (other.races != null) - return false; - } else { - // compare race column names only, not the contents - if (races.size() != (other.races==null?0:other.races.size())) { - return false; - } - List raceColumnNames = new ArrayList(races.size()); - List otherRaceColumnNames = new ArrayList(races.size()); - for (RaceColumnDTO race : races) { - raceColumnNames.add(race.getName()); - } - if (other.races != null) { - for (RaceColumnDTO otherRace : other.races) { - otherRaceColumnNames.add(otherRace.getName()); - } - } - if (!raceColumnNames.equals(otherRaceColumnNames)) - return false; - if (rows == null) { - if (other.rows != null) - return false; - } else if (!rows.equals(other.rows)) - return false; - } - return true; - } - - public boolean isDisplayNameSet(CompetitorDTO competitor) { - return competitorDisplayNames.get(competitor) != null; - } - - public Long getDelayToLiveInMillisForLatestRace() { - return delayToLiveInMillisForLatestRace; - } - - public void setDelayToLiveInMillisForLatestRace(Long delayToLiveInMillisForLatestRace) { - this.delayToLiveInMillisForLatestRace = delayToLiveInMillisForLatestRace; - } -} +package com.sap.sailing.domain.common.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sse.common.Util; + +public abstract class AbstractLeaderboardDTO implements Serializable { + private static final long serialVersionUID = -205106531931903527L; + + public String name; + + private List races; + public Map competitorDisplayNames; + public Map rows; + public boolean hasCarriedPoints; + public int[] discardThresholds; + + /** + * Set to the non-null regatta name if this DTO represents a RegattaLeaderboard. + */ + public String regattaName; + public String displayName; + public UUID defaultCourseAreaId; + public String defaultCourseAreaName; + public ScoringSchemeType scoringScheme; + public LeaderboardType type; + + private Long delayToLiveInMillisForLatestRace; + + public AbstractLeaderboardDTO() { + races = new ArrayList(); + } + + public Set getBoatClasses() { + Set result = new HashSet(); + if(rows != null) { + for (CompetitorDTO competitor : rows.keySet()) { + result.add(competitor.getBoatClass()); + } + } + return result; + } + + public String getDisplayName() { + return displayName; + } + + public String getDisplayName(CompetitorDTO competitor) { + if (competitorDisplayNames == null || competitorDisplayNames.get(competitor) == null) { + return competitor.getName(); + } else { + return competitorDisplayNames.get(competitor); + } + } + + /** + * If the race column whose name is specified in raceColumnName has at least one competitor who has valid + * {@link LeaderboardEntryDTO#legDetails} for that race column, the maximum number of entries of all such competitors in + * the leg details is returned, telling the number of legs that the race column shall display. Otherwise, -1 is returned. + * If you specify an non null preselectedRace the leg count is calculated only for that race for the column + * holding that race. All other columns will be unaffected by preselectedRace and will return their regular + * leg count. Note that this may lead to a situation where some competitors may have sailed more legs than others because + * their fleet association may have varied across races.

    + * + * See also bug 2604 and bug 2035. + */ + public int getLegCount(String raceColumnName, RaceIdentifier preselectedRace) { + int result = -1; + final boolean preselectedRaceIsInRaceColumn = isRaceInColumn(preselectedRace, raceColumnName); + for (LeaderboardRowDTO row : rows.values()) { + LeaderboardEntryDTO leaderboardEntryDTO = row.fieldsByRaceColumnName.get(raceColumnName); + if (leaderboardEntryDTO != null && leaderboardEntryDTO.legDetails != null + // when no race is pre-selected, always use the actual leg count; + // otherwise, if the pre-selected race is in the column identified by + // raceColumnName, use the entry only if it's in the pre-selected race; + // otherwise (a race is pre-selected but a different column's leg count + // is requested), use the regular leg count as is. + && (preselectedRace == null || !preselectedRaceIsInRaceColumn || preselectedRace.equals(leaderboardEntryDTO.race))) { + result = Math.max(result, leaderboardEntryDTO.legDetails.size()); + } + } + return result; + } + + private boolean isRaceInColumn(RaceIdentifier preselectedRace, String raceColumnName) { + RaceColumnDTO raceColumn = getRaceColumnByName(raceColumnName); + return raceColumn.containsRace(preselectedRace); + } + + /** + * Tells if the competitor scored (and therefore presumably participated) in a medal race represented + * in this leaderboard. + */ + public boolean scoredInMedalRace(CompetitorDTO competitor) { + LeaderboardRowDTO row = rows.get(competitor); + for (RaceColumnDTO race : races) { + if (race.isMedalRace() && row.fieldsByRaceColumnName.get(race.getRaceColumnName()).totalPoints > 0) { + return true; + } + } + return false; + } + + public Double getNetPoints(CompetitorDTO competitor, String nameOfLastRaceSoFar) { + Double netPoints = null; + LeaderboardRowDTO row = rows.get(competitor); + if (row != null) { + LeaderboardEntryDTO field = row.fieldsByRaceColumnName.get(nameOfLastRaceSoFar); + if (field != null && field.netPoints != null) { + netPoints = field.netPoints; + } + } + return netPoints; + } + + public boolean raceIsTracked(String raceColumnName) { + for (RaceColumnDTO race : races) { + if (race.getRaceColumnName().equals(raceColumnName)) { + for (FleetDTO fleet : race.getFleets()) { + if (race.isTrackedRace(fleet)) { + return true; + } + } + } + } + return false; + + } + + public boolean raceIsMedalRace(String raceColumnName) { + return getRaceColumnByName(raceColumnName).isMedalRace(); + } + + /** + * If the {@link RaceColumnDTO} by the name raceColumnName doesn't exist yet within this leaderboard + * DTO, it is created, setting is {@link RaceColumnDTO#isValidInTotalScore()} to true. This method + * ensures that a fleet named fleetName is present. If it's not present yet, it's added to the race + * column's fleet name list. The trackedRaceIdentifier and race are associated with the + * column for the fleet identified by fleetName. + * + * @param explicitFactor + * factor by which to multiply the race column's points for the overall score; if null, the + * default will be determined by whether or not the column is marked as medal race + * @param effectiveFactor + * is what you get when you call {@link RaceColumn#getFactor()} on the race column that the resulting + * {@link RaceColumnDTO} represents + * @param regattaName + * must not be null if seriesName is not null; specified + * separately from trackedRaceIdentifier because a column may belong to regatta/series + * despite not having a tracked race associated + * @param seriesName + * if null, this method will produce a {@link RaceColumnDTO}, otherwise a + * {@link RaceColumnInSeriesDTO} + * @param fleetDTO + * must not be null + */ + public RaceColumnDTO addRace(String raceColumnName, Double explicitFactor, double effectiveFactor, + String regattaName, String seriesName, FleetDTO fleetDTO, boolean medalRace, + RegattaAndRaceIdentifier trackedRaceIdentifier, RaceDTO race, boolean isMetaLeaderboardColumn) { + assert fleetDTO != null; + RaceColumnDTO raceColumnDTO = getRaceColumnByName(raceColumnName); + if (raceColumnDTO == null) { + raceColumnDTO = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, medalRace, + explicitFactor, regattaName, seriesName, isMetaLeaderboardColumn); + races.add(raceColumnDTO); + } + raceColumnDTO.setEffectiveFactor(effectiveFactor); + boolean contains = false; + for (FleetDTO fleet : raceColumnDTO.getFleets()) { + if (fleet.getName().equals(fleetDTO.getName())) { + contains = true; + break; + } + } + if (!contains) { + raceColumnDTO.addFleet(fleetDTO); + } + raceColumnDTO.setRaceIdentifier(fleetDTO, trackedRaceIdentifier); + raceColumnDTO.setRace(fleetDTO, race); + return raceColumnDTO; + } + + public RaceColumnDTO createEmptyRaceColumn(String raceColumnName, boolean medalRace, String regattaName, + String seriesName, boolean isMetaLeaderboardColumn) { + final RaceColumnDTO raceColumn = RaceColumnDTOFactory.INSTANCE.createRaceColumnDTO(raceColumnName, + medalRace, /* explicit factor */ null, regattaName, seriesName, isMetaLeaderboardColumn); + races.add(raceColumn); + return raceColumn; + } + + public RaceColumnDTO getRaceColumnByName(String raceColumnName) { + for (RaceColumnDTO race : races) { + if (race.getRaceColumnName().equals(raceColumnName)) { + return race; + } + } + return null; + } + + public int getRaceColumnsCount() { + return races.size(); + } + + public int getRacesCount() { + int result = 0; + for (RaceColumnDTO race : getRaceList()) { + result += race.getFleets().size(); + } + return result; + } + + public int getTrackedRacesCount() { + int result = 0; + for (RaceColumnDTO raceColumn : getRaceList()) { + for (FleetDTO fleet : raceColumn.getFleets()) { + RaceDTO race = raceColumn.getRace(fleet); + if(race != null && race.trackedRace != null && race.trackedRace.hasGPSData && race.trackedRace.hasWindData) { + result++; + } + } + } + return result; + } + + public List getRaceList() { + return races; + } + + public void setRaceList(List raceList) { + this.races = raceList; + } + + public boolean raceListContains(String raceColumnName) { + return getRaceColumnByName(raceColumnName) != null; + } + + public void moveRaceUp(String raceColumnName) { + RaceColumnDTO race = getRaceColumnByName(raceColumnName); + int index = races.indexOf(race); + index--; + if (index >= 0) { + races.remove(index + 1); + races.add(index, race); + } + } + + public void moveRaceDown(String raceColumnName) { + RaceColumnDTO race = getRaceColumnByName(raceColumnName); + int index = races.indexOf(race); + if (index != -1) { + index++; + if (index < races.size()) { + races.remove(index - 1); + races.add(index, race); + } + } + } + + public void setIsMedalRace(String raceColumnName, boolean medalRace) { + getRaceColumnByName(raceColumnName).setMedalRace(medalRace); + } + + /** + * @return The earliest start date of the races, or null if no start dates of the races are available. + */ + public Date getStartDate() { + Date leaderboardStart = null; + for (RaceColumnDTO race : getRaceList()) { + for (FleetDTO fleet: race.getFleets()) { + Date raceStart = race.getStartDate(fleet); + if (raceStart != null) { + if (leaderboardStart == null) { + leaderboardStart = new Date(); + } else { + leaderboardStart = leaderboardStart.before(raceStart) ? leaderboardStart : raceStart; + } + } + } + } + return leaderboardStart; + } + + /** + * Takes the {@link PlacemarkOrderDTO} of all races in this leaderboard, if the PlacemarkOrderDTO for the race is + * available, and fills all {@link PlacemarkDTO} in a new PlacemarkOrderDTO.
    + * The order of the races in this leaderboard determine the order of the PlacemarkDTOs in the PlacemarkOrderDTO. + * + * @return The places of this leaderboard in form of a {@link PlacemarkOrderDTO}, or null if the + * {@link PlacemarkOrderDTO places} of no race are available + */ + public PlacemarkOrderDTO getPlaces() { + PlacemarkOrderDTO leaderboardPlaces = null; + for (RaceColumnDTO race : getRaceList()) { + PlacemarkOrderDTO racePlaces = race.getPlaces(); + if (racePlaces != null) { + if (leaderboardPlaces == null) { + leaderboardPlaces = new PlacemarkOrderDTO(); + } + leaderboardPlaces.getPlacemarks().addAll(racePlaces.getPlacemarks()); + } + } + return leaderboardPlaces; + } + + /** + * @return true if the leaderboard contains a race which is live + */ + public boolean hasLiveRace(long serverTimePointAsMillis) { + for (RaceColumnDTO race : getRaceList()) { + for (FleetDTO fleet : race.getFleets()) { + if (race.isLive(fleet, serverTimePointAsMillis)) { + return true; + } + } + } + return false; + } + + public List> getLiveRaces(long serverTimePointAsMillis) { + List> result = new ArrayList>(); + for (RaceColumnDTO race : getRaceList()) { + for (FleetDTO fleet : race.getFleets()) { + if (race.isLive(fleet, serverTimePointAsMillis)) { + result.add(new Util.Pair(race, fleet)); + } + } + } + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((competitorDisplayNames == null) ? 0 : competitorDisplayNames.hashCode()); + result = prime * result + Arrays.hashCode(discardThresholds); + result = prime * result + (hasCarriedPoints ? 1231 : 1237); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((scoringScheme == null) ? 0 : scoringScheme.hashCode()); + if (races == null) { + result = prime * result; + } else { + List raceNames = new ArrayList(); + for (RaceColumnDTO race : races) { + raceNames.add(race.getName()); + } + result = prime * result + raceNames.hashCode(); + } + result = prime * result + ((rows == null) ? 0 : rows.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AbstractLeaderboardDTO other = (AbstractLeaderboardDTO) obj; + if (competitorDisplayNames == null) { + if (other.competitorDisplayNames != null) + return false; + } else if (!competitorDisplayNames.equals(other.competitorDisplayNames)) + return false; + if (!Arrays.equals(discardThresholds, other.discardThresholds)) + return false; + if (hasCarriedPoints != other.hasCarriedPoints) + return false; + if (type != other.type) + return false; + if (scoringScheme != other.scoringScheme) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (races == null) { + if (other.races != null) + return false; + } else { + // compare race column names only, not the contents + if (races.size() != (other.races==null?0:other.races.size())) { + return false; + } + List raceColumnNames = new ArrayList(races.size()); + List otherRaceColumnNames = new ArrayList(races.size()); + for (RaceColumnDTO race : races) { + raceColumnNames.add(race.getName()); + } + if (other.races != null) { + for (RaceColumnDTO otherRace : other.races) { + otherRaceColumnNames.add(otherRace.getName()); + } + } + if (!raceColumnNames.equals(otherRaceColumnNames)) + return false; + if (rows == null) { + if (other.rows != null) + return false; + } else if (!rows.equals(other.rows)) + return false; + } + return true; + } + + public boolean isDisplayNameSet(CompetitorDTO competitor) { + return competitorDisplayNames.get(competitor) != null; + } + + public Long getDelayToLiveInMillisForLatestRace() { + return delayToLiveInMillisForLatestRace; + } + + public void setDelayToLiveInMillisForLatestRace(Long delayToLiveInMillisForLatestRace) { + this.delayToLiveInMillisForLatestRace = delayToLiveInMillisForLatestRace; + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java new file mode 100644 index 00000000000..8938136783c --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/BasicRaceDTO.java @@ -0,0 +1,112 @@ +package com.sap.sailing.domain.common.dto; + +import java.util.Date; + +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.TimingConstants; + +/** + * Master data about a single race that is to be transferred to the client. Holds only timing and a bit + * of information about the tracked race, if any. See also {@link RaceDTO} for a more comprehensive set + * of data about a race to be serialized to a client.

    + * + * @author Axel Uhl (d043530) + * + */ +public class BasicRaceDTO extends NamedDTO { + private static final long serialVersionUID = -7884808503795229609L; + + public Date startOfRace; + public Date endOfRace; + public TrackedRaceDTO trackedRace; + + public BasicRaceDTO() {} // for GWT serialization only + + public BasicRaceDTO(RegattaAndRaceIdentifier raceIdentifier, TrackedRaceDTO trackedRace) { + super(raceIdentifier.getRaceName()); + this.trackedRace = trackedRace; + } + + /** + * @see {@link TrackedRace#isLive} for further explanation. + * @param serverTimePointAsMillis + * the time point (in server clock time) at which to determine whether the race is/was live + * @return true if serverTimePointAsMillis is between (inclusively) the start and end time + * point of the "live" interval as defined above. + */ + public boolean isLive(long serverTimePointAsMillis) { + final Date startOfLivePeriod; + final Date endOfLivePeriod; + if (trackedRace == null || !trackedRace.hasGPSData || !trackedRace.hasWindData) { + startOfLivePeriod = null; + endOfLivePeriod = null; + } else { + if (startOfRace == null) { + startOfLivePeriod = trackedRace.startOfTracking; + } else { + startOfLivePeriod = new Date(startOfRace.getTime() - TimingConstants.PRE_START_PHASE_DURATION_IN_MILLIS); + } + if (endOfRace == null) { + if (trackedRace.timePointOfNewestEvent != null) { + endOfLivePeriod = new Date(trackedRace.timePointOfNewestEvent.getTime() + + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); + } else { + endOfLivePeriod = null; + } + } else { + endOfLivePeriod = new Date(endOfRace.getTime() + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); + } + } + + // if an empty timepoint is given then take the start of the race + if (serverTimePointAsMillis == 0) { + serverTimePointAsMillis = startOfLivePeriod.getTime()+1; + } + + // whenLastTrackedRaceWasLive is null if there is no tracked race for fleet, or the tracked race hasn't started yet at the server time + // when this DTO was assembled, or there were no GPS or wind data + final boolean result = + startOfLivePeriod != null && + endOfLivePeriod != null && + startOfLivePeriod.getTime() <= serverTimePointAsMillis && + serverTimePointAsMillis <= endOfLivePeriod.getTime(); + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((endOfRace == null) ? 0 : endOfRace.hashCode()); + result = prime * result + ((startOfRace == null) ? 0 : startOfRace.hashCode()); + result = prime * result + ((trackedRace == null) ? 0 : trackedRace.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + BasicRaceDTO other = (BasicRaceDTO) obj; + if (endOfRace == null) { + if (other.endOfRace != null) + return false; + } else if (!endOfRace.equals(other.endOfRace)) + return false; + if (startOfRace == null) { + if (other.startOfRace != null) + return false; + } else if (!startOfRace.equals(other.startOfRace)) + return false; + if (trackedRace == null) { + if (other.trackedRace != null) + return false; + } else if (!trackedRace.equals(other.trackedRace)) + return false; + return true; + } +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java index 64012fec26c..dd9b5e94981 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/LeaderboardDTO.java @@ -62,7 +62,6 @@ public class LeaderboardDTO extends AbstractLeaderboardDTO implements Serializab /** * @param uuidGenerator used to provide the {@link #id ID} for this object (see also {@link #getId()}) and for any clones produced * from it by the {@link #clone()} operation. - * @param hasOverallDetails TODO */ public LeaderboardDTO(Date timePointOfLastCorrectionsValidity, String comment, ScoringSchemeType scoringScheme, boolean higherScoreIsBetter, UUIDGenerator uuidGenerator, boolean hasOverallDetails) { initCollections(); @@ -79,7 +78,7 @@ public class LeaderboardDTO extends AbstractLeaderboardDTO implements Serializab this.suppressedCompetitors = new ArrayList(); } - public LeaderboardDTO(String id) { + protected LeaderboardDTO(String id) { initCollections(); this.id = id; } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java new file mode 100644 index 00000000000..475718b447b --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/MetaLeaderboardRaceColumnDTO.java @@ -0,0 +1,49 @@ +package com.sap.sailing.domain.common.dto; + +import java.util.ArrayList; + +public class MetaLeaderboardRaceColumnDTO extends RaceColumnDTO { + + private static final long serialVersionUID = 6152752963316150432L; + + private ArrayList raceList = new ArrayList(); + + public boolean isLive(FleetDTO fleet, long serverTimePointAsMillis) { + for (BasicRaceDTO race : raceList) { + if (race.isLive(serverTimePointAsMillis)) { + return true; + } + } + return false; + } + + public void addRace(BasicRaceDTO race) { + raceList.add(race); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((raceList == null) ? 0 : raceList.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + MetaLeaderboardRaceColumnDTO other = (MetaLeaderboardRaceColumnDTO) obj; + if (raceList == null) { + if (other.raceList != null) + return false; + } else if (!raceList.equals(other.raceList)) + return false; + return true; + } + +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java index 7d8a0efbb67..46083549e93 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceColumnDTOFactory.java @@ -7,15 +7,17 @@ public class RaceColumnDTOFactory { * @param regattaName * must not be null if seriesName is not null * @param seriesName - * when null, a regular {@link RaceColumnDTO} will be produced and initialized from the - * properties passed; otherwise, a {@link RaceColumnInSeriesDTO} will be created. + * when null, a regular {@link RaceColumnDTO} or {@link MetaLeaderboardRaceColumnDTO} will + * be produced (depending on isMetaLeaderboardColumn) and initialized from the properties + * passed; otherwise, a {@link RaceColumnInSeriesDTO} will be created. */ - public RaceColumnDTO createRaceColumnDTO(String columnName, boolean isMedal, Double explicitFactor, String regattaName, String seriesName) { + public RaceColumnDTO createRaceColumnDTO(String columnName, boolean isMedal, Double explicitFactor, + String regattaName, String seriesName, boolean isMetaLeaderboardColumn) { final RaceColumnDTO raceColumnDTO; if (seriesName != null) { raceColumnDTO = new RaceColumnInSeriesDTO(seriesName, regattaName); } else { - raceColumnDTO = new RaceColumnDTO(); + raceColumnDTO = isMetaLeaderboardColumn ? new MetaLeaderboardRaceColumnDTO() : new RaceColumnDTO(); } fillRaceColumnDTO(raceColumnDTO, columnName, isMedal, explicitFactor); return raceColumnDTO; diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java index eba9caf39b7..6a8e90c617f 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/dto/RaceDTO.java @@ -1,10 +1,7 @@ package com.sap.sailing.domain.common.dto; -import java.util.Date; - import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.TimingConstants; /** * Master data about a single race that is to be transferred to the client.

    @@ -12,7 +9,7 @@ import com.sap.sailing.domain.common.TimingConstants; * @author Axel Uhl (d043530) * */ -public class RaceDTO extends NamedDTO { +public class RaceDTO extends BasicRaceDTO { private static final long serialVersionUID = 2613189982608149975L; public enum RaceLiveState { NOT_TRACKED, TRACKED, TRACKED_AND_LIVE, TRACKED_BUT_NOT_SCHEDULED }; @@ -24,13 +21,10 @@ public class RaceDTO extends NamedDTO { */ public boolean isTracked; - public Date startOfRace; - public Date endOfRace; public RaceStatusDTO status; public PlacemarkOrderDTO places; - public TrackedRaceDTO trackedRace; public TrackedRaceStatisticsDTO trackedRaceStatistics; private String regattaName; @@ -43,21 +37,20 @@ public class RaceDTO extends NamedDTO { } public RaceDTO(RegattaAndRaceIdentifier raceIdentifier, TrackedRaceDTO trackedRace, boolean isCurrentlyTracked) { - super(raceIdentifier.getRaceName()); + super(raceIdentifier, trackedRace); this.regattaName = raceIdentifier.getRegattaName(); - this.trackedRace = trackedRace; this.isTracked = isCurrentlyTracked; } public RaceLiveState getLiveState(long serverTimePointAsMillis) { - RaceLiveState result = RaceLiveState.NOT_TRACKED; - if(trackedRace != null && trackedRace.hasGPSData && trackedRace.hasWindData) { + RaceLiveState result = RaceLiveState.NOT_TRACKED; + if (trackedRace != null && trackedRace.hasGPSData && trackedRace.hasWindData) { result = RaceLiveState.TRACKED; - if(isLive(serverTimePointAsMillis)) { + if (isLive(serverTimePointAsMillis)) { result = RaceLiveState.TRACKED_AND_LIVE; - if(startOfRace == null) { + if (startOfRace == null) { result = RaceLiveState.TRACKED_BUT_NOT_SCHEDULED; - } + } } } return result; @@ -71,64 +64,15 @@ public class RaceDTO extends NamedDTO { return regattaName; } - /** - * @see {@link TrackedRace#isLive} for further explanation. - * @param serverTimePointAsMillis - * the time point (in server clock time) at which to determine whether the race is/was live - * @return true if serverTimePointAsMillis is between (inclusively) the start and end time - * point of the "live" interval as defined above. - */ - public boolean isLive(long serverTimePointAsMillis) { - final Date startOfLivePeriod; - final Date endOfLivePeriod; - if (trackedRace == null || !trackedRace.hasGPSData || !trackedRace.hasWindData) { - startOfLivePeriod = null; - endOfLivePeriod = null; - } else { - if (startOfRace == null) { - startOfLivePeriod = trackedRace.startOfTracking; - } else { - startOfLivePeriod = new Date(startOfRace.getTime() - TimingConstants.PRE_START_PHASE_DURATION_IN_MILLIS); - } - if (endOfRace == null) { - if (trackedRace.timePointOfNewestEvent != null) { - endOfLivePeriod = new Date(trackedRace.timePointOfNewestEvent.getTime() - + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); - } else { - endOfLivePeriod = null; - } - } else { - endOfLivePeriod = new Date(endOfRace.getTime() + TimingConstants.IS_LIVE_GRACE_PERIOD_IN_MILLIS); - } - } - - // if an empty timepoint is given then take the start of the race - if (serverTimePointAsMillis == 0) { - serverTimePointAsMillis = startOfLivePeriod.getTime()+1; - } - - // whenLastTrackedRaceWasLive is null if there is no tracked race for fleet, or the tracked race hasn't started yet at the server time - // when this DTO was assembled, or there were no GPS or wind data - final boolean result = - startOfLivePeriod != null && - endOfLivePeriod != null && - startOfLivePeriod.getTime() <= serverTimePointAsMillis && - serverTimePointAsMillis <= endOfLivePeriod.getTime(); - return result; - } - @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + ((boatClass == null) ? 0 : boatClass.hashCode()); - result = prime * result + ((endOfRace == null) ? 0 : endOfRace.hashCode()); result = prime * result + (isTracked ? 1231 : 1237); result = prime * result + ((places == null) ? 0 : places.hashCode()); result = prime * result + ((regattaName == null) ? 0 : regattaName.hashCode()); - result = prime * result + ((startOfRace == null) ? 0 : startOfRace.hashCode()); result = prime * result + ((status == null) ? 0 : status.hashCode()); - result = prime * result + ((trackedRace == null) ? 0 : trackedRace.hashCode()); return result; } @@ -146,11 +90,6 @@ public class RaceDTO extends NamedDTO { return false; } else if (!boatClass.equals(other.boatClass)) return false; - if (endOfRace == null) { - if (other.endOfRace != null) - return false; - } else if (!endOfRace.equals(other.endOfRace)) - return false; if (isTracked != other.isTracked) return false; if (places == null) { @@ -163,21 +102,11 @@ public class RaceDTO extends NamedDTO { return false; } else if (!regattaName.equals(other.regattaName)) return false; - if (startOfRace == null) { - if (other.startOfRace != null) - return false; - } else if (!startOfRace.equals(other.startOfRace)) - return false; if (status == null) { if (other.status != null) return false; } else if (!status.equals(other.status)) return false; - if (trackedRace == null) { - if (other.trackedRace != null) - return false; - } else if (!trackedRace.equals(other.trackedRace)) - return false; return true; } } diff --git a/java/com.sap.sailing.domain.persistence/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.persistence/META-INF/MANIFEST.MF index c169680edd6..6b35a388318 100644 --- a/java/com.sap.sailing.domain.persistence/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.persistence/META-INF/MANIFEST.MF @@ -14,7 +14,8 @@ Require-Bundle: com.sap.sse.mongodb, com.sap.sailing.server.gateway.serialization, com.sap.sse.common, com.sap.sse, - com.sap.sailing.server.gateway.serialization.shared.android + com.sap.sailing.server.gateway.serialization.shared.android, + com.sap.sse.shared.android Export-Package: com.sap.sailing.domain.persistence, com.sap.sailing.domain.persistence.impl; x-friends:="com.sap.sailing.domain.persistence.test, diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java index 80050c13ee2..d83d3620927 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/DomainObjectFactoryImpl.java @@ -224,11 +224,11 @@ import com.sap.sse.common.impl.MillisecondsDurationImpl; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.common.impl.RGBColor; import com.sap.sse.common.impl.TimeRangeImpl; -import com.sap.sse.common.media.ImageDescriptor; -import com.sap.sse.common.media.ImageDescriptorImpl; import com.sap.sse.common.media.MimeType; -import com.sap.sse.common.media.VideoDescriptor; -import com.sap.sse.common.media.VideoDescriptorImpl; +import com.sap.sse.shared.media.ImageDescriptor; +import com.sap.sse.shared.media.VideoDescriptor; +import com.sap.sse.shared.media.impl.ImageDescriptorImpl; +import com.sap.sse.shared.media.impl.VideoDescriptorImpl; public class DomainObjectFactoryImpl implements DomainObjectFactory { private static final Logger logger = Logger.getLogger(DomainObjectFactoryImpl.class.getName()); diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java index a0c0ae078f3..0bbc0721096 100644 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/MongoObjectFactoryImpl.java @@ -1,1533 +1,1533 @@ -package com.sap.sailing.domain.persistence.impl; - -import java.io.Serializable; -import java.net.URL; -import java.util.List; -import java.util.Map.Entry; -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.bson.types.ObjectId; -import org.json.simple.JSONObject; - -import com.mongodb.BasicDBList; -import com.mongodb.BasicDBObject; -import com.mongodb.BasicDBObjectBuilder; -import com.mongodb.DB; -import com.mongodb.DBCollection; -import com.mongodb.DBObject; -import com.mongodb.WriteConcern; -import com.mongodb.util.JSON; -import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningConfirmedEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningListChangedEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogFixedMarkPassingEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogFlagEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogGateLineOpeningTimeEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogPassChangeEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogPathfinderEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogProtestStartTimeEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogRaceStatusEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogRevokeEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; -import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; -import com.sap.sailing.domain.abstractlog.race.scoring.RaceLogAdditionalScoringInformationEvent; -import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogCloseOpenEndedDeviceMappingEvent; -import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDefineMarkEvent; -import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; -import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDeviceCompetitorMappingEvent; -import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDeviceMarkMappingEvent; -import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogRegisterCompetitorEvent; -import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogStartTrackingEvent; -import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEvent; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogCloseOpenEndedDeviceMappingEvent; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceCompetitorMappingEvent; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceMarkMappingEvent; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogRegisterCompetitorEvent; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogRevokeEvent; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogSetCompetitorTimeOnDistanceAllowancePerNauticalMileEvent; -import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogSetCompetitorTimeOnTimeFactorEvent; -import com.sap.sailing.domain.abstractlog.shared.events.DeviceMappingEvent; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.ControlPoint; -import com.sap.sailing.domain.base.ControlPointWithTwoMarks; -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.CourseBase; -import com.sap.sailing.domain.base.Event; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.RemoteSailingServerReference; -import com.sap.sailing.domain.base.SailingServerConfiguration; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.Venue; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.base.configuration.DeviceConfiguration; -import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; -import com.sap.sailing.domain.base.configuration.RegattaConfiguration; -import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationMatcherMulti; -import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationMatcherSingle; -import com.sap.sailing.domain.base.impl.FleetImpl; -import com.sap.sailing.domain.common.Bearing; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.PassingInstruction; -import com.sap.sailing.domain.common.Positioned; -import com.sap.sailing.domain.common.RaceIdentifier; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.Wind; -import com.sap.sailing.domain.common.WindSource; -import com.sap.sailing.domain.common.racelog.tracking.TransformationException; -import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.persistence.MongoObjectFactory; -import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; -import com.sap.sailing.domain.persistence.racelog.tracking.impl.PlaceHolderDeviceIdentifierMongoHandler; -import com.sap.sailing.domain.racelog.RaceLogIdentifier; -import com.sap.sailing.domain.racelogtracking.DeviceIdentifier; -import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.domain.tracking.TrackedRegatta; -import com.sap.sailing.domain.tracking.WindTrack; -import com.sap.sailing.server.gateway.serialization.JsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.CompetitorJsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.DeviceConfigurationJsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.RegattaConfigurationJsonSerializer; -import com.sap.sse.common.Duration; -import com.sap.sse.common.NoCorrespondingServiceRegisteredException; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.TimeRange; -import com.sap.sse.common.Timed; -import com.sap.sse.common.TypeBasedServiceFinder; -import com.sap.sse.common.TypeBasedServiceFinderFactory; -import com.sap.sse.common.Util; -import com.sap.sse.common.impl.MillisecondsTimePoint; -import com.sap.sse.common.media.ImageDescriptor; -import com.sap.sse.common.media.VideoDescriptor; - -public class MongoObjectFactoryImpl implements MongoObjectFactory { - private static Logger logger = Logger.getLogger(MongoObjectFactoryImpl.class.getName()); - private final DB database; - private final CompetitorJsonSerializer competitorSerializer = CompetitorJsonSerializer.create(); - private final TypeBasedServiceFinder deviceIdentifierServiceFinder; - - /** - * Uses null for the device type service finder and hence will be unable to store device identifiers. - * Use this constructor only for testing purposes or in cases where there will happen absolutely no access to - * {@link DeviceIdentifier} objects. - */ - public MongoObjectFactoryImpl(DB database) { - this(database, /* deviceTypeServiceFinder */ null); - } - - public MongoObjectFactoryImpl(DB database, TypeBasedServiceFinderFactory serviceFinderFactory) { - this.database = database; - if (serviceFinderFactory != null) { - this.deviceIdentifierServiceFinder = serviceFinderFactory.createServiceFinder(DeviceIdentifierMongoHandler.class); - this.deviceIdentifierServiceFinder.setFallbackService(new PlaceHolderDeviceIdentifierMongoHandler()); - } else { - this.deviceIdentifierServiceFinder = null; - } - } - - @Override - public DB getDatabase() { - return database; - } - - public DBObject storeWind(Wind wind) { - DBObject result = new BasicDBObject(); - storePositioned(wind, result); - storeTimed(wind, result); - storeSpeedWithBearing(wind, result); - return result; - } - - public static void storeTimePoint(TimePoint timePoint, DBObject result, String fieldName) { - if (timePoint != null) { - result.put(fieldName, timePoint.asMillis()); - } - } - - public static void storeTimePoint(TimePoint timePoint, DBObject result, FieldNames field) { - storeTimePoint(timePoint, result, field.name()); - } - - public static void storeTimeRange(TimeRange timeRange, DBObject result, FieldNames field) { - if (timeRange != null) { - DBObject timeRangeObj = new BasicDBObject(); - storeTimePoint(timeRange.from(), timeRangeObj, FieldNames.FROM_MILLIS); - storeTimePoint(timeRange.to(), timeRangeObj, FieldNames.TO_MILLIS); - result.put(field.name(), timeRangeObj); - } - } - - public void storeTimed(Timed timed, DBObject result) { - if (timed.getTimePoint() != null) { - storeTimePoint(timed.getTimePoint(), result, FieldNames.TIME_AS_MILLIS); - } - } - - public void storeSpeedWithBearing(SpeedWithBearing speedWithBearing, DBObject result) { - storeSpeed(speedWithBearing, result); - storeBearing(speedWithBearing.getBearing(), result); - - } - - public void storeBearing(Bearing bearing, DBObject result) { - result.put(FieldNames.DEGREE_BEARING.name(), bearing.getDegrees()); - } - - public void storeSpeed(Speed speed, DBObject result) { - result.put(FieldNames.KNOT_SPEED.name(), speed.getKnots()); - } - - public void storePositioned(Positioned positioned, DBObject result) { - if (positioned.getPosition() != null) { - result.put(FieldNames.LAT_DEG.name(), positioned.getPosition().getLatDeg()); - result.put(FieldNames.LNG_DEG.name(), positioned.getPosition().getLngDeg()); - } - } - - @Override - public void addWindTrackDumper(TrackedRegatta trackedRegatta, TrackedRace trackedRace, WindSource windSource) { - WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); - windTrack.addListener(new MongoWindListener(trackedRace, trackedRegatta.getRegatta().getName(), windSource, this, database)); - } - - public DBCollection getWindTrackCollection() { - DBCollection result = database.getCollection(CollectionNames.WIND_TRACKS.name()); - result.createIndex(new BasicDBObject(FieldNames.REGATTA_NAME.name(), null)); - return result; - } - - public DBCollection getGPSFixCollection() { - DBCollection gpsFixCollection = database.getCollection(CollectionNames.GPS_FIXES.name()); - DBObject index = new BasicDBObject(); - index.put(FieldNames.DEVICE_ID.name(), null); - index.put(FieldNames.TIME_AS_MILLIS.name(), null); - gpsFixCollection.createIndex(index); - return gpsFixCollection; - } - - public DBCollection getGPSFixMetadataCollection() { - DBCollection collection = database.getCollection(CollectionNames.GPS_FIXES_METADATA.name()); - DBObject index = new BasicDBObject(); - index.put(FieldNames.DEVICE_ID.name(), null); - collection.createIndex(index); - return collection; - } - - /** - * @param regattaName - * the regatta name is stored only for human readability purposes because a time stamp may be a bit unhandy for - * identifying where the wind fix was collected - */ - public DBObject storeWindTrackEntry(RaceDefinition race, String regattaName, WindSource windSource, Wind wind) { - BasicDBObject result = new BasicDBObject(); - result.put(FieldNames.RACE_ID.name(), race.getId()); - result.put(FieldNames.REGATTA_NAME.name(), regattaName); - result.put(FieldNames.WIND_SOURCE_NAME.name(), windSource.name()); - if (windSource.getId() != null) { - result.put(FieldNames.WIND_SOURCE_ID.name(), windSource.getId()); - } - result.put(FieldNames.WIND.name(), storeWind(wind)); - return result; - } - - private void storeRaceIdentifiers(RaceColumn raceColumn, DBObject dbObject) { - BasicDBObject raceIdentifiersPerFleet = new BasicDBObject(); - for (Fleet fleet : raceColumn.getFleets()) { - RaceIdentifier raceIdentifier = raceColumn.getRaceIdentifier(fleet); - if (raceIdentifier != null) { - DBObject raceIdentifierForFleet = new BasicDBObject(); - storeRaceIdentifier(raceIdentifierForFleet, raceIdentifier); - raceIdentifiersPerFleet.put(MongoUtils.escapeDollarAndDot(fleet.getName()), raceIdentifierForFleet); - } - } - dbObject.put(FieldNames.RACE_IDENTIFIERS.name(), raceIdentifiersPerFleet); - } - - private void storeRaceIdentifier(DBObject dbObject, RaceIdentifier raceIdentifier) { - if (raceIdentifier != null) { - dbObject.put(FieldNames.EVENT_NAME.name(), raceIdentifier.getRegattaName()); - dbObject.put(FieldNames.RACE_NAME.name(), raceIdentifier.getRaceName()); - } - } - - @Override - public void storeLeaderboard(Leaderboard leaderboard) { - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - try { - leaderboardCollection.createIndex(new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), 1)); - } catch (NullPointerException npe) { - // sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an NPE - logger.log(Level.SEVERE, "storeLeaderboard", npe); - } - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); - BasicDBObject dbLeaderboard = new BasicDBObject(); - dbLeaderboard.put(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); - if (leaderboard.getDisplayName() != null) { - dbLeaderboard.put(FieldNames.LEADERBOARD_DISPLAY_NAME.name(), leaderboard.getDisplayName()); - } - BasicDBList dbSuppressedCompetitorIds = new BasicDBList(); - for (Competitor suppressedCompetitor : leaderboard.getSuppressedCompetitors()) { - dbSuppressedCompetitorIds.add(suppressedCompetitor.getId()); - } - dbLeaderboard.put(FieldNames.LEADERBOARD_SUPPRESSED_COMPETITOR_IDS.name(), dbSuppressedCompetitorIds); - if (leaderboard instanceof FlexibleLeaderboard) { - storeFlexibleLeaderboard((FlexibleLeaderboard) leaderboard, dbLeaderboard); - } else if (leaderboard instanceof RegattaLeaderboard) { - storeRegattaLeaderboard((RegattaLeaderboard) leaderboard, dbLeaderboard); - } else { - // at least store the scoring scheme - dbLeaderboard.put(FieldNames.SCORING_SCHEME_TYPE.name(), leaderboard.getScoringScheme().getType().name()); - } - if (leaderboard.getDefaultCourseArea() != null) { - dbLeaderboard.put(FieldNames.COURSE_AREA_ID.name(), leaderboard.getDefaultCourseArea().getId().toString()); - } else { - dbLeaderboard.put(FieldNames.COURSE_AREA_ID.name(), null); - } - storeColumnFactors(leaderboard, dbLeaderboard); - storeLeaderboardCorrectionsAndDiscards(leaderboard, dbLeaderboard); - leaderboardCollection.update(query, dbLeaderboard, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } - - private void storeColumnFactors(Leaderboard leaderboard, BasicDBObject dbLeaderboard) { - DBObject raceColumnFactors = new BasicDBObject(); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - Double explicitFactor = raceColumn.getExplicitFactor(); - if (explicitFactor != null) { - raceColumnFactors.put(MongoUtils.escapeDollarAndDot(raceColumn.getName()), explicitFactor); - } - } - dbLeaderboard.put(FieldNames.LEADERBOARD_COLUMN_FACTORS.name(), raceColumnFactors); - } - - private void storeRegattaLeaderboard(RegattaLeaderboard leaderboard, DBObject dbLeaderboard) { - dbLeaderboard.put(FieldNames.REGATTA_NAME.name(), leaderboard.getRegatta().getName()); - } - - private void storeFlexibleLeaderboard(FlexibleLeaderboard leaderboard, BasicDBObject dbLeaderboard) { - BasicDBList dbRaceColumns = new BasicDBList(); - dbLeaderboard.put(FieldNames.SCORING_SCHEME_TYPE.name(), leaderboard.getScoringScheme().getType().name()); - dbLeaderboard.put(FieldNames.LEADERBOARD_COLUMNS.name(), dbRaceColumns); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - BasicDBObject dbRaceColumn = storeRaceColumn(raceColumn); - dbRaceColumns.add(dbRaceColumn); - } - } - - private void storeLeaderboardCorrectionsAndDiscards(Leaderboard leaderboard, BasicDBObject dbLeaderboard) { - if (leaderboard.hasCarriedPoints()) { - BasicDBList dbCarriedPoints = new BasicDBList(); - dbLeaderboard.put(FieldNames.LEADERBOARD_CARRIED_POINTS_BY_ID.name(), dbCarriedPoints); - for (Entry competitorWithCarriedPoints : leaderboard - .getCompetitorsForWhichThereAreCarriedPoints().entrySet()) { - double carriedPoints = competitorWithCarriedPoints.getValue(); - Competitor competitor = competitorWithCarriedPoints.getKey(); - DBObject dbCarriedPointsForCompetitor = new BasicDBObject(); - dbCarriedPointsForCompetitor.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); - dbCarriedPointsForCompetitor.put(FieldNames.LEADERBOARD_CARRIED_POINTS.name(), carriedPoints); - dbCarriedPoints.add(dbCarriedPointsForCompetitor); - } - } - BasicDBObject dbScoreCorrections = new BasicDBObject(); - storeScoreCorrections(leaderboard, dbScoreCorrections); - dbLeaderboard.put(FieldNames.LEADERBOARD_SCORE_CORRECTIONS.name(), dbScoreCorrections); - final ResultDiscardingRule resultDiscardingRule = leaderboard.getResultDiscardingRule(); - storeResultDiscardingRule(dbLeaderboard, resultDiscardingRule, FieldNames.LEADERBOARD_DISCARDING_THRESHOLDS); - BasicDBList competitorDisplayNames = new BasicDBList(); - for (Competitor competitor : leaderboard.getCompetitors()) { - String displayNameForCompetitor = leaderboard.getDisplayName(competitor); - if (displayNameForCompetitor != null) { - DBObject dbDisplayName = new BasicDBObject(); - dbDisplayName.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); - dbDisplayName.put(FieldNames.COMPETITOR_DISPLAY_NAME.name(), displayNameForCompetitor); - competitorDisplayNames.add(dbDisplayName); - } - } - dbLeaderboard.put(FieldNames.LEADERBOARD_COMPETITOR_DISPLAY_NAMES.name(), competitorDisplayNames); - } - - /** - * Stores the result discarding rule to dbObject's field identified by field if the result discarding - * rule is not null and is of type {@link ThresholdBasedResultDiscardingRule}. Otherwise, it is assumed that the - * result discarding rule is otherwise implicitly obtained, e.g., from a definition of a regatta with its series, stored elsewhere. - */ - private void storeResultDiscardingRule(DBObject dbObject, - final ResultDiscardingRule resultDiscardingRule, FieldNames field) { - if (resultDiscardingRule != null && resultDiscardingRule instanceof ThresholdBasedResultDiscardingRule) { - BasicDBList dbResultDiscardingThresholds = new BasicDBList(); - for (int threshold : ((ThresholdBasedResultDiscardingRule) resultDiscardingRule).getDiscardIndexResultsStartingWithHowManyRaces()) { - dbResultDiscardingThresholds.add(threshold); - } - dbObject.put(field.name(), dbResultDiscardingThresholds); - } - } - - private BasicDBObject storeRaceColumn(RaceColumn raceColumn) { - BasicDBObject dbRaceColumn = new BasicDBObject(); - dbRaceColumn.put(FieldNames.LEADERBOARD_COLUMN_NAME.name(), raceColumn.getName()); - dbRaceColumn.put(FieldNames.LEADERBOARD_IS_MEDAL_RACE_COLUMN.name(), raceColumn.isMedalRace()); - storeRaceIdentifiers(raceColumn, dbRaceColumn); - return dbRaceColumn; - } - - private void storeScoreCorrections(Leaderboard leaderboard, BasicDBObject dbScoreCorrections) { - TimePoint now = MillisecondsTimePoint.now(); - SettableScoreCorrection scoreCorrection = leaderboard.getScoreCorrection(); - for (RaceColumn raceColumn : scoreCorrection.getRaceColumnsThatHaveCorrections()) { - BasicDBList dbCorrectionForRace = new BasicDBList(); - for (Competitor competitor : scoreCorrection.getCompetitorsThatHaveCorrectionsIn(raceColumn)) { - // TODO bug 655: make score corrections time dependent - if (scoreCorrection.isScoreCorrected(competitor, raceColumn, now)) { - BasicDBObject dbCorrectionForCompetitor = new BasicDBObject(); - dbCorrectionForCompetitor.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); - MaxPointsReason maxPointsReason = scoreCorrection.getMaxPointsReason(competitor, raceColumn, now); - if (maxPointsReason != MaxPointsReason.NONE) { - dbCorrectionForCompetitor.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name(), - maxPointsReason.name()); - } - Double explicitScoreCorrection = scoreCorrection - .getExplicitScoreCorrection(competitor, raceColumn); - if (explicitScoreCorrection != null) { - dbCorrectionForCompetitor.put(FieldNames.LEADERBOARD_CORRECTED_SCORE.name(), - explicitScoreCorrection); - } - dbCorrectionForRace.add(dbCorrectionForCompetitor); - } - } - if (!dbCorrectionForRace.isEmpty()) { - // using the column name as the key for the score corrections requires re-writing the score corrections - // of a meta-leaderboard if the name of one of its leaderboards changes - dbScoreCorrections.put(MongoUtils.escapeDollarAndDot(raceColumn.getName()), dbCorrectionForRace); - } - } - final TimePoint timePointOfLastCorrectionsValidity = scoreCorrection.getTimePointOfLastCorrectionsValidity(); - if (timePointOfLastCorrectionsValidity != null) { - dbScoreCorrections.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_TIMESTAMP.name(), timePointOfLastCorrectionsValidity.asMillis()); - } - if (scoreCorrection.getComment() != null) { - dbScoreCorrections.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_COMMENT.name(), scoreCorrection.getComment()); - } - } - - @Override - public void removeLeaderboard(String leaderboardName) { - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboardName); - leaderboardCollection.remove(query); - } - - @Override - public void renameLeaderboard(String oldName, String newName) { - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), oldName); - BasicDBObject renameUpdate = new BasicDBObject("$set", new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), newName)); - leaderboardCollection.update(query, renameUpdate, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void storeLeaderboardGroup(LeaderboardGroup leaderboardGroup) { - DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); - DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); - - try { - leaderboardGroupCollection.createIndex(new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), 1)); - } catch (NullPointerException npe) { - // sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an NPE - logger.log(Level.SEVERE, "storeLeaderboardGroup", npe); - } - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), leaderboardGroup.getName()); - BasicDBObject dbLeaderboardGroup = new BasicDBObject(); - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_UUID.name(), leaderboardGroup.getId()); - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_NAME.name(), leaderboardGroup.getName()); - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_DESCRIPTION.name(), leaderboardGroup.getDescription()); - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_DISPLAY_NAME.name(), leaderboardGroup.getDisplayName()); - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_DISPLAY_IN_REVERSE_ORDER.name(), leaderboardGroup.isDisplayGroupsInReverseOrder()); - final Leaderboard overallLeaderboard = leaderboardGroup.getOverallLeaderboard(); - if (overallLeaderboard != null) { - BasicDBObject overallLeaderboardQuery = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), overallLeaderboard.getName()); - DBObject dbOverallLeaderboard = leaderboardCollection.findOne(overallLeaderboardQuery); - if (dbOverallLeaderboard == null) { - storeLeaderboard(overallLeaderboard); - dbOverallLeaderboard = leaderboardCollection.findOne(overallLeaderboardQuery); - } - ObjectId dbOverallLeaderboardId = (ObjectId) dbOverallLeaderboard.get("_id"); - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_OVERALL_LEADERBOARD.name(), dbOverallLeaderboardId); - } - BasicDBList dbLeaderboardIds = new BasicDBList(); - for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { - BasicDBObject leaderboardQuery = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); - DBObject dbLeaderboard = leaderboardCollection.findOne(leaderboardQuery); - if (dbLeaderboard == null) { - storeLeaderboard(leaderboard); - dbLeaderboard = leaderboardCollection.findOne(leaderboardQuery); - } - ObjectId dbLeaderboardId = (ObjectId) dbLeaderboard.get("_id"); - dbLeaderboardIds.add(dbLeaderboardId); - } - dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_LEADERBOARDS.name(), dbLeaderboardIds); - leaderboardGroupCollection.update(query, dbLeaderboardGroup, true, false, WriteConcern.SAFE); - } - - @Override - public void removeLeaderboardGroup(String groupName) { - DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), groupName); - leaderboardGroupCollection.remove(query); - } - - @Override - public void renameLeaderboardGroup(String oldName, String newName) { - DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), oldName); - BasicDBObject update = new BasicDBObject("$set", new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), newName)); - leaderboardGroupCollection.update(query, update, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void storeServerConfiguration(SailingServerConfiguration serverConfiguration) { - DBCollection serverCollection = database.getCollection(CollectionNames.SERVER_CONFIGURATION.name()); - DBObject newServerConfig = new BasicDBObject(); - newServerConfig.put(FieldNames.SERVER_IS_STANDALONE.name(), serverConfiguration.isStandaloneServer()); - DBObject currentServerConfig = serverCollection.findOne(); - if(currentServerConfig != null) { - serverCollection.update(currentServerConfig, newServerConfig, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } else { - serverCollection.save(newServerConfig); - } - } - - @Override - public void storeSailingServer(RemoteSailingServerReference server) { - DBCollection serverCollection = database.getCollection(CollectionNames.SAILING_SERVERS.name()); - serverCollection.createIndex(new BasicDBObject(FieldNames.SERVER_NAME.name(), 1)); - DBObject query = new BasicDBObject(); - query.put(FieldNames.SERVER_NAME.name(), server.getName()); - DBObject serverDBObject = new BasicDBObject(); - serverDBObject.put(FieldNames.SERVER_NAME.name(), server.getName()); - serverDBObject.put(FieldNames.SERVER_URL.name(), server.getURL().toExternalForm()); - serverCollection.update(query, serverDBObject, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void removeSailingServer(String name) { - DBCollection serverCollection = database.getCollection(CollectionNames.SAILING_SERVERS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.SERVER_NAME.name(), name); - serverCollection.remove(query); - } - - /** - * StoreEvent() uses some deprecated methods of event to keep backward compatibility. - */ - @SuppressWarnings("deprecation") - @Override - public void storeEvent(Event event) { - DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); - eventCollection.createIndex(new BasicDBObject(FieldNames.EVENT_ID.name(), 1)); - DBObject query = new BasicDBObject(); - query.put(FieldNames.EVENT_ID.name(), event.getId()); - DBObject eventDBObject = new BasicDBObject(); - eventDBObject.put(FieldNames.EVENT_NAME.name(), event.getName()); - eventDBObject.put(FieldNames.EVENT_DESCRIPTION.name(), event.getDescription()); - eventDBObject.put(FieldNames.EVENT_ID.name(), event.getId()); - eventDBObject.put(FieldNames.EVENT_LOGO_IMAGE_URL.name(), event.getLogoImageURL() != null ? event.getLogoImageURL().toString() : null); - eventDBObject.put(FieldNames.EVENT_OFFICIAL_WEBSITE_URL.name(), event.getOfficialWebsiteURL() != null ? event.getOfficialWebsiteURL().toString() : null); - eventDBObject.put(FieldNames.EVENT_SAILORS_INFO_WEBSITE_URL.name(), event.getSailorsInfoWebsiteURL() != null ? event.getSailorsInfoWebsiteURL().toString() : null); - storeTimePoint(event.getStartDate(), eventDBObject, FieldNames.EVENT_START_DATE); - storeTimePoint(event.getEndDate(), eventDBObject, FieldNames.EVENT_END_DATE); - eventDBObject.put(FieldNames.EVENT_IS_PUBLIC.name(), event.isPublic()); - DBObject venueDBObject = getVenueAsDBObject(event.getVenue()); - eventDBObject.put(FieldNames.VENUE.name(), venueDBObject); - BasicDBList imageURLs = new BasicDBList(); - for (URL imageURL : event.getImageURLs()) { - imageURLs.add(imageURL.toString()); - } - eventDBObject.put(FieldNames.EVENT_IMAGE_URLS.name(), imageURLs); - BasicDBList videoURLs = new BasicDBList(); - for (URL videoURL : event.getVideoURLs()) { - videoURLs.add(videoURL.toString()); - } - eventDBObject.put(FieldNames.EVENT_VIDEO_URLS.name(), videoURLs); - BasicDBList sponsorImageURLs = new BasicDBList(); - for (URL sponsorImageURL : event.getSponsorImageURLs()) { - sponsorImageURLs.add(sponsorImageURL.toString()); - } - eventDBObject.put(FieldNames.EVENT_SPONSOR_IMAGE_URLS.name(), sponsorImageURLs); - BasicDBList images = new BasicDBList(); - for (ImageDescriptor image : event.getImages()) { - DBObject imageObject = createImageObject(image); - images.add(imageObject); - } - eventDBObject.put(FieldNames.EVENT_IMAGES.name(), images); - BasicDBList videos = new BasicDBList(); - for (VideoDescriptor video: event.getVideos()) { - DBObject videoObject = createVideoObject(video); - videos.add(videoObject); - } - eventDBObject.put(FieldNames.EVENT_VIDEOS.name(), videos); - eventCollection.update(query, eventDBObject, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - // now store the links to the leaderboard groups - DBCollection linksCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUP_LINKS_FOR_EVENTS.name()); - linksCollection.createIndex(new BasicDBObject(FieldNames.EVENT_ID.name(), 1)); - BasicDBList lgUUIDs = new BasicDBList(); - for (LeaderboardGroup lg : event.getLeaderboardGroups()) { - lgUUIDs.add(lg.getId()); - } - DBObject dbLinks = new BasicDBObject(); - dbLinks.put(FieldNames.EVENT_ID.name(), event.getId()); - dbLinks.put(FieldNames.LEADERBOARD_GROUP_UUID.name(), lgUUIDs); - linksCollection.update(query, dbLinks, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void renameEvent(Serializable id, String newName) { - DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.EVENT_ID.name(), id); - BasicDBObject renameUpdate = new BasicDBObject("$set", new BasicDBObject(FieldNames.EVENT_NAME.name(), newName)); - eventCollection.update(query, renameUpdate, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void removeEvent(Serializable id) { - DBCollection eventsCollection = database.getCollection(CollectionNames.EVENTS.name()); - BasicDBObject query = new BasicDBObject(FieldNames.EVENT_ID.name(), id); - eventsCollection.remove(query); - } - - private DBObject getVenueAsDBObject(Venue venue) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.VENUE_NAME.name(), venue.getName()); - BasicDBList courseAreaList = new BasicDBList(); - result.put(FieldNames.COURSE_AREAS.name(), courseAreaList); - for (CourseArea courseArea : venue.getCourseAreas()) { - DBObject dbCourseArea = new BasicDBObject(); - courseAreaList.add(dbCourseArea); - dbCourseArea.put(FieldNames.COURSE_AREA_NAME.name(), courseArea.getName()); - dbCourseArea.put(FieldNames.COURSE_AREA_ID.name(), courseArea.getId()); - } - return result; - } - - @Override - public void storeRegatta(Regatta regatta) { - DBCollection regattasCollection = database.getCollection(CollectionNames.REGATTAS.name()); - regattasCollection.createIndex(new BasicDBObject(FieldNames.REGATTA_NAME.name(), 1)); - regattasCollection.createIndex(new BasicDBObject(FieldNames.REGATTA_ID.name(), 1)); - DBObject dbRegatta = new BasicDBObject(); - DBObject query = new BasicDBObject(FieldNames.REGATTA_NAME.name(), regatta.getName()); - dbRegatta.put(FieldNames.REGATTA_NAME.name(), regatta.getName()); - dbRegatta.put(FieldNames.REGATTA_ID.name(), regatta.getId()); - storeTimePoint(regatta.getStartDate(), dbRegatta, FieldNames.REGATTA_START_DATE); - storeTimePoint(regatta.getEndDate(), dbRegatta, FieldNames.REGATTA_END_DATE); - dbRegatta.put(FieldNames.SCORING_SCHEME_TYPE.name(), regatta.getScoringScheme().getType().name()); - if (regatta.getBoatClass() != null) { - dbRegatta.put(FieldNames.BOAT_CLASS_NAME.name(), regatta.getBoatClass().getName()); - dbRegatta.put(FieldNames.BOAT_CLASS_TYPICALLY_STARTS_UPWIND.name(), regatta.getBoatClass().typicallyStartsUpwind()); - } - dbRegatta.put(FieldNames.REGATTA_SERIES.name(), storeSeries(regatta.getSeries())); - - if (regatta.getDefaultCourseArea() != null) { - dbRegatta.put(FieldNames.COURSE_AREA_ID.name(), regatta.getDefaultCourseArea().getId().toString()); - } else { - dbRegatta.put(FieldNames.COURSE_AREA_ID.name(), null); - } - if (regatta.getRegattaConfiguration() != null) { - JsonSerializer serializer = RegattaConfigurationJsonSerializer.create(); - JSONObject json = serializer.serialize(regatta.getRegattaConfiguration()); - DBObject configurationObject = (DBObject) JSON.parse(json.toString()); - dbRegatta.put(FieldNames.REGATTA_REGATTA_CONFIGURATION.name(), configurationObject); - } - dbRegatta.put(FieldNames.REGATTA_USE_START_TIME_INFERENCE.name(), regatta.useStartTimeInference()); - dbRegatta.put(FieldNames.REGATTA_RANKING_METRIC.name(), storeRankingMetric(regatta)); - regattasCollection.update(query, dbRegatta, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } - - private DBObject storeRankingMetric(Regatta regatta) { - DBObject rankingMetricJson = new BasicDBObject(); - final String rankingMetricTypeName = regatta.getRankingMetricType().name(); - rankingMetricJson.put(FieldNames.REGATTA_RANKING_METRIC_TYPE.name(), rankingMetricTypeName); - return rankingMetricJson; - } - - @Override - public void removeRegatta(Regatta regatta) { - DBCollection regattasCollection = database.getCollection(CollectionNames.REGATTAS.name()); - DBObject query = new BasicDBObject(FieldNames.REGATTA_NAME.name(), regatta.getName()); - regattasCollection.remove(query); - } - - private BasicDBList storeSeries(Iterable series) { - BasicDBList dbSeries = new BasicDBList(); - for (Series s : series) { - dbSeries.add(storeSeries(s)); - } - return dbSeries; - } - - private DBObject storeSeries(Series s) { - DBObject dbSeries = new BasicDBObject(); - dbSeries.put(FieldNames.SERIES_NAME.name(), s.getName()); - dbSeries.put(FieldNames.SERIES_IS_MEDAL.name(), s.isMedal()); - dbSeries.put(FieldNames.SERIES_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING.name(), s.hasSplitFleetContiguousScoring()); - dbSeries.put(FieldNames.SERIES_STARTS_WITH_ZERO_SCORE.name(), s.isStartsWithZeroScore()); - dbSeries.put(FieldNames.SERIES_STARTS_WITH_NON_DISCARDABLE_CARRY_FORWARD.name(), s.isFirstColumnIsNonDiscardableCarryForward()); - BasicDBList dbFleets = new BasicDBList(); - for (Fleet fleet : s.getFleets()) { - dbFleets.add(storeFleet(fleet)); - } - dbSeries.put(FieldNames.SERIES_FLEETS.name(), dbFleets); - BasicDBList dbRaceColumns = new BasicDBList(); - for (RaceColumn raceColumn : s.getRaceColumns()) { - dbRaceColumns.add(storeRaceColumn(raceColumn)); - } - dbSeries.put(FieldNames.SERIES_RACE_COLUMNS.name(), dbRaceColumns); - if (s.getResultDiscardingRule() != null) { - storeResultDiscardingRule(dbSeries, s.getResultDiscardingRule(), FieldNames.SERIES_DISCARDING_THRESHOLDS); - } - return dbSeries; - } - - private DBObject storeFleet(Fleet fleet) { - DBObject dbFleet = new BasicDBObject(FieldNames.FLEET_NAME.name(), fleet.getName()); - if (fleet instanceof FleetImpl) { - dbFleet.put(FieldNames.FLEET_ORDERING.name(), ((FleetImpl) fleet).getOrdering()); - if(fleet.getColor() != null) { - com.sap.sse.common.Util.Triple colorAsRGB = fleet.getColor().getAsRGB(); - // we save the color as a integer value representing the RGB values - int colorAsInt = (256 * 256 * colorAsRGB.getC()) + colorAsRGB.getB() * 256 + colorAsRGB.getA(); - dbFleet.put(FieldNames.FLEET_COLOR.name(), colorAsInt); - } else { - dbFleet.put(FieldNames.FLEET_COLOR.name(), null); - } - } - return dbFleet; - } - - @Override - public void storeRegattaForRaceID(String raceIDAsString, Regatta regatta) { - DBCollection regattaForRaceIDCollection = database.getCollection(CollectionNames.REGATTA_FOR_RACE_ID.name()); - DBObject query = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); - DBObject entry = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); - entry.put(FieldNames.REGATTA_NAME.name(), regatta.getName()); - regattaForRaceIDCollection.update(query, entry, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } - - @Override - public void removeRegattaForRaceID(String raceIDAsString, Regatta regatta) { - DBCollection regattaForRaceIDCollection = database.getCollection(CollectionNames.REGATTA_FOR_RACE_ID.name()); - DBObject query = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); - regattaForRaceIDCollection.remove(query); - } - - public DBCollection getRaceLogCollection() { - DBCollection result = database.getCollection(CollectionNames.RACE_LOGS.name()); - result.createIndex(new BasicDBObject(FieldNames.RACE_LOG_IDENTIFIER.name(), null)); - return result; - } - - private void storeRaceLogEventAuthor(DBObject dbObject, AbstractLogEventAuthor author) { - if (author != null) { - dbObject.put(FieldNames.RACE_LOG_EVENT_AUTHOR_NAME.name(), author.getName()); - dbObject.put(FieldNames.RACE_LOG_EVENT_AUTHOR_PRIORITY.name(), author.getPriority()); - } - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFlagEvent flagEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFlagEvent(flagEvent)); - return result; - } - - private void storeRaceLogIdentifier(RaceLogIdentifier raceLogIdentifier, DBObject result) { - result.put(FieldNames.RACE_LOG_IDENTIFIER.name(), TripleSerializer.serialize(raceLogIdentifier.getIdentifier())); - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartTimeEvent startTimeEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartTimeEvent(startTimeEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogPassChangeEvent passChangeEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogPassChangeEvent(passChangeEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogRaceStatusEvent raceStatusEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogRaceStatusEvent(raceStatusEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCourseAreaChangedEvent courseAreaChangedEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCourseAreaChangedEvent(courseAreaChangedEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCourseDesignChangedEvent courseDesignChangedEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCourseDesignChangedEvent(courseDesignChangedEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFinishPositioningListChangedEvent finishPositioningListChangedEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFinishPositioningListChangedEvent(finishPositioningListChangedEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFinishPositioningConfirmedEvent finishPositioningConfirmedEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFinishPositioningConfirmedEvent(finishPositioningConfirmedEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogPathfinderEvent pathfinderEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogPathfinderEvent(pathfinderEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogGateLineOpeningTimeEvent gateLineOpeningTimeEvent) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogGateLineOpeningTimeEvent(gateLineOpeningTimeEvent)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartProcedureChangedEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartProcedureChangedEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogProtestStartTimeEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogProtestStartTimeEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogWindFixEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogWindFix(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogDeviceCompetitorMappingEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDeviceCompetitorMappingEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogDeviceMarkMappingEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDeviceMarkMappingEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogDenoteForTrackingEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDenoteForTrackingEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartTrackingEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartTrackingEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogRevokeEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogRevokeEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogRegisterCompetitorEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogRegisterCompetitorEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogDefineMarkEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDefineMarkEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCloseOpenEndedDeviceMappingEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCloseOpenEndedDeviceMappingEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogEndOfTrackingEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogEndOfTrackingEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartOfTrackingEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartOfTrackingEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogAdditionalScoringInformationEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeAdditionalScoringInformation(event)); - return result; - } - - private Object storeAdditionalScoringInformation(RaceLogAdditionalScoringInformationEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogAdditionalScoringInformationEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_ADDITIONAL_SCORING_INFORMATION_TYPE.name(), event.getType().name()); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFixedMarkPassingEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFixedMarkPassingEvent(event)); - return result; - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogSuppressedMarkPassingsEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogSuppressedMarkPassingsEvent(event)); - return result; - } - - private Object storeRaceLogWindFix(RaceLogWindFixEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogWindFixEvent.class.getSimpleName()); - result.put(FieldNames.WIND.name(), storeWind(event.getWindFix())); - result.put(FieldNames.IS_MAGNETIC.name(), event.isMagnetic()); - return result; - } - - private Object storeRaceLogProtestStartTimeEvent(RaceLogProtestStartTimeEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogProtestStartTimeEvent.class.getSimpleName()); - storeTimePoint(event.getProtestStartTime(), result, FieldNames.RACE_LOG_PROTEST_START_TIME); - return result; - } - - private Object storeRaceLogEndOfTrackingEvent(RaceLogEndOfTrackingEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogEndOfTrackingEvent.class.getSimpleName()); - return result; - } - - private Object storeRaceLogStartOfTrackingEvent(RaceLogStartOfTrackingEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartOfTrackingEvent.class.getSimpleName()); - return result; - } - - private Object storeRaceLogStartProcedureChangedEvent(RaceLogStartProcedureChangedEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartProcedureChangedEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_START_PROCEDURE_TYPE.name(), event.getStartProcedureType().name()); - return result; - } - - private Object storeRaceLogPathfinderEvent(RaceLogPathfinderEvent pathfinderEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(pathfinderEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogPathfinderEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_PATHFINDER_ID.name(), pathfinderEvent.getPathfinderId()); - return result; - } - - private void storeDeviceMappingEvent(DeviceMappingEvent event, DBObject result, FieldNames fromField, FieldNames toField) { - try { - result.put(FieldNames.DEVICE_ID.name(), storeDeviceId(deviceIdentifierServiceFinder, event.getDevice())); - } catch (TransformationException | NoCorrespondingServiceRegisteredException e) { - logger.log(Level.WARNING, "Could not store device identifier for mappng event", e); - } - if (event.getFrom() != null) { - storeTimePoint(event.getFrom(), result, fromField); - } - if (event.getTo() != null) { - storeTimePoint(event.getTo(), result, toField); - } - } - - private Object storeRaceLogDeviceCompetitorMappingEvent(RaceLogDeviceCompetitorMappingEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogDeviceCompetitorMappingEvent.class.getSimpleName()); - storeDeviceMappingEvent(event, result, FieldNames.RACE_LOG_FROM, FieldNames.RACE_LOG_TO); - result.put(FieldNames.COMPETITOR_ID.name(), event.getMappedTo().getId()); - return result; - } - - private Object storeRaceLogDeviceMarkMappingEvent(RaceLogDeviceMarkMappingEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogDeviceMarkMappingEvent.class.getSimpleName()); - storeDeviceMappingEvent(event, result, FieldNames.RACE_LOG_FROM, FieldNames.RACE_LOG_TO); - result.put(FieldNames.MARK.name(), storeMark(event.getMappedTo())); - return result; - } - - private Object storeRaceLogDenoteForTrackingEvent(RaceLogDenoteForTrackingEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogDenoteForTrackingEvent.class.getSimpleName()); - result.put(FieldNames.RACE_NAME.name(), event.getRaceName()); - result.put(FieldNames.BOAT_CLASS_NAME.name(), event.getBoatClass().getName()); - result.put(FieldNames.RACE_ID.name(), event.getRaceId()); - return result; - } - - private Object storeRaceLogStartTrackingEvent(RaceLogStartTrackingEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartTrackingEvent.class.getSimpleName()); - return result; - } - - private Object storeRaceLogRevokeEvent(RaceLogRevokeEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogRevokeEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_REVOKED_EVENT_ID.name(), event.getRevokedEventId()); - result.put(FieldNames.RACE_LOG_REVOKED_EVENT_TYPE.name(), event.getRevokedEventType()); - result.put(FieldNames.RACE_LOG_REVOKED_EVENT_SHORT_INFO.name(), event.getRevokedEventShortInfo()); - result.put(FieldNames.RACE_LOG_REVOKED_REASON.name(), event.getReason()); - return result; - } - - private Object storeRaceLogRegisterCompetitorEvent(RaceLogRegisterCompetitorEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogRegisterCompetitorEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_COMPETITOR_ID.name(), event.getCompetitor().getId()); - return result; - } - - private Object storeRaceLogDefineMarkEvent(RaceLogDefineMarkEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogDefineMarkEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_MARK.name(), storeMark(event.getMark())); - return result; - } - - private Object storeRaceLogCloseOpenEndedDeviceMappingEvent(RaceLogCloseOpenEndedDeviceMappingEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogCloseOpenEndedDeviceMappingEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_DEVICE_MAPPING_EVENT_ID.name(), event.getDeviceMappingEventId()); - storeTimePoint(event.getClosingTimePoint(), result, FieldNames.RACE_LOG_CLOSING_TIMEPOINT); - return result; - } - - private Object storeRaceLogFixedMarkPassingEvent(RaceLogFixedMarkPassingEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFixedMarkPassingEvent.class.getSimpleName()); - result.put(FieldNames.INDEX_OF_PASSED_WAYPOINT.name(), event.getZeroBasedIndexOfPassedWaypoint()); - result.put(FieldNames.TIMEPOINT_OF_FIXED_MARKPASSING.name(), event.getTimePointOfFixedPassing().asMillis()); - return result; - } - - private Object storeRaceLogSuppressedMarkPassingsEvent(RaceLogSuppressedMarkPassingsEvent event) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(event, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogSuppressedMarkPassingsEvent.class.getSimpleName()); - result.put(FieldNames.INDEX_OF_FIRST_SUPPRESSED_WAYPOINT.name(), event.getZeroBasedIndexOfFirstSuppressedWaypoint()); - return result; - } - - public DBObject storeRaceLogFlagEvent(RaceLogFlagEvent flagEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(flagEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFlagEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_EVENT_FLAG_UPPER.name(), flagEvent.getUpperFlag().name()); - result.put(FieldNames.RACE_LOG_EVENT_FLAG_LOWER.name(), flagEvent.getLowerFlag().name()); - result.put(FieldNames.RACE_LOG_EVENT_FLAG_DISPLAYED.name(), String.valueOf(flagEvent.isDisplayed())); - return result; - } - - private DBObject storeRaceLogStartTimeEvent(RaceLogStartTimeEvent startTimeEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(startTimeEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartTimeEvent.class.getSimpleName()); - storeTimePoint(startTimeEvent.getStartTime(), result, FieldNames.RACE_LOG_EVENT_START_TIME); - result.put(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name(), startTimeEvent.getNextStatus().name()); - return result; - } - - private void storeRaceLogEventProperties(RaceLogEvent event, DBObject result) { - // for compatibility reasons we reuse the field name of Timed - storeTimePoint(event.getLogicalTimePoint(), result, FieldNames.TIME_AS_MILLIS); - storeTimePoint(event.getCreatedAt(), result, FieldNames.RACE_LOG_EVENT_CREATED_AT); - result.put(FieldNames.RACE_LOG_EVENT_ID.name(), event.getId()); - result.put(FieldNames.RACE_LOG_EVENT_PASS_ID.name(), event.getPassId()); - result.put(FieldNames.RACE_LOG_EVENT_INVOLVED_BOATS.name(), storeInvolvedBoatsForRaceLogEvent(event.getInvolvedBoats())); - storeRaceLogEventAuthor(result, event.getAuthor()); - } - - - private BasicDBList storeInvolvedBoatsForRaceLogEvent(List competitors) { - BasicDBList dbInvolvedCompetitorIds = new BasicDBList(); - for (Competitor competitor : competitors) { - dbInvolvedCompetitorIds.add(competitor.getId()); - } - return dbInvolvedCompetitorIds; - } - - private DBObject storeRaceLogPassChangeEvent(RaceLogPassChangeEvent passChangeEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(passChangeEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogPassChangeEvent.class.getSimpleName()); - return result; - } - - private DBObject storeRaceLogDependentStartTimeEvent(RaceLogDependentStartTimeEvent dependentStartTimeEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(dependentStartTimeEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogDependentStartTimeEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_DEPDENDENT_ON_REGATTALIKE.name(), dependentStartTimeEvent.getDependentOnRaceIdentifier().getRegattaLikeParentName()); - result.put(FieldNames.RACE_LOG_DEPDENDENT_ON_RACECOLUMN.name(), dependentStartTimeEvent.getDependentOnRaceIdentifier().getRaceColumnName()); - result.put(FieldNames.RACE_LOG_DEPDENDENT_ON_FLEET.name(), dependentStartTimeEvent.getDependentOnRaceIdentifier().getFleetName()); - storeDuration(dependentStartTimeEvent.getStartTimeDifference(), result, FieldNames.RACE_LOG_START_TIME_DIFFERENCE_IN_MS); - result.put(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name(), dependentStartTimeEvent.getNextStatus().name()); - return result; - } - - private void storeDuration(Duration duration, DBObject result, FieldNames fieldName) { - if (duration != null) { - result.put(fieldName.name(), duration.asMillis()); - } - } - - private DBObject storeRaceLogRaceStatusEvent(RaceLogRaceStatusEvent raceStatusEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(raceStatusEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogRaceStatusEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name(), raceStatusEvent.getNextStatus().name()); - return result; - } - - private DBObject storeRaceLogCourseAreaChangedEvent(RaceLogCourseAreaChangedEvent courseAreaChangedEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(courseAreaChangedEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogCourseAreaChangedEvent.class.getSimpleName()); - result.put(FieldNames.COURSE_AREA_ID.name(), courseAreaChangedEvent.getCourseAreaId()); - return result; - } - - private DBObject storeRaceLogCourseDesignChangedEvent(RaceLogCourseDesignChangedEvent courseDesignChangedEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(courseDesignChangedEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogCourseDesignChangedEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_COURSE_DESIGN_NAME.name(), courseDesignChangedEvent.getCourseDesign().getName()); - result.put(FieldNames.RACE_LOG_COURSE_DESIGN.name(), storeCourseBase(courseDesignChangedEvent.getCourseDesign())); - return result; - } - - private Object storeRaceLogFinishPositioningListChangedEvent(RaceLogFinishPositioningListChangedEvent finishPositioningListChangedEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(finishPositioningListChangedEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFinishPositioningListChangedEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name(), storePositionedCompetitors(finishPositioningListChangedEvent.getPositionedCompetitorsIDsNamesMaxPointsReasons())); - - return result; - } - - private Object storeRaceLogFinishPositioningConfirmedEvent(RaceLogFinishPositioningConfirmedEvent finishPositioningConfirmedEvent) { - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(finishPositioningConfirmedEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFinishPositioningConfirmedEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name(), storePositionedCompetitors(finishPositioningConfirmedEvent.getPositionedCompetitorsIDsNamesMaxPointsReasons())); - - return result; - } - - private Object storeRaceLogGateLineOpeningTimeEvent(RaceLogGateLineOpeningTimeEvent gateLineOpeningTimeEvent){ - DBObject result = new BasicDBObject(); - storeRaceLogEventProperties(gateLineOpeningTimeEvent, result); - result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogGateLineOpeningTimeEvent.class.getSimpleName()); - result.put(FieldNames.RACE_LOG_GATE_LINE_OPENING_TIME.name(), gateLineOpeningTimeEvent.getGateLineOpeningTimes().getGateLaunchStopTime()); - result.put(FieldNames.RACE_LOG_GOLF_DOWN_TIME.name(), gateLineOpeningTimeEvent.getGateLineOpeningTimes().getGolfDownTime()); - return result; - } - - private BasicDBList storePositionedCompetitors(List> positionedCompetitors) { - BasicDBList dbList = new BasicDBList(); - if (positionedCompetitors != null) { - for (com.sap.sse.common.Util.Triple competitorPair : positionedCompetitors) { - dbList.add(storePositionedCompetitor(competitorPair)); - } - } - return dbList; - } - - private DBObject storePositionedCompetitor(com.sap.sse.common.Util.Triple competitorTriple) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.COMPETITOR_ID.name(), competitorTriple.getA()); - result.put(FieldNames.COMPETITOR_DISPLAY_NAME.name(), competitorTriple.getB()); - result.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name(), competitorTriple.getC().name()); - - return result; - } - - private BasicDBList storeCourseBase(CourseBase courseData) { - BasicDBList dbList = new BasicDBList(); - - for (Waypoint waypoint : courseData.getWaypoints()) { - dbList.add(storeWaypoint(waypoint)); - } - return dbList; - } - - private DBObject storeWaypoint(Waypoint waypoint) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.WAYPOINT_PASSINGINSTRUCTIONS.name(), getPassingInstructions(waypoint.getPassingInstructions())); - result.put(FieldNames.CONTROLPOINT.name(), storeControlPoint(waypoint.getControlPoint())); - return result; - } - - private DBObject storeControlPoint(ControlPoint controlPoint) { - DBObject result = new BasicDBObject(); - if (controlPoint instanceof Mark) { - result.put(FieldNames.CONTROLPOINT_CLASS.name(), Mark.class.getSimpleName()); - result.put(FieldNames.CONTROLPOINT_VALUE.name(), storeMark((Mark) controlPoint)); - } else if (controlPoint instanceof ControlPointWithTwoMarks) { - result.put(FieldNames.CONTROLPOINT_CLASS.name(), ControlPointWithTwoMarks.class.getSimpleName()); - result.put(FieldNames.CONTROLPOINT_VALUE.name(), storeControlPointWithTwoMarks((ControlPointWithTwoMarks) controlPoint)); - } - return result; - } - - private DBObject storeControlPointWithTwoMarks(ControlPointWithTwoMarks cpwtm) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_ID.name(), cpwtm.getId()); - result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_NAME.name(), cpwtm.getName()); - result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_LEFT.name(), storeMark(cpwtm.getLeft())); - result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_RIGHT.name(), storeMark(cpwtm.getRight())); - return result; - } - - private DBObject storeMark(Mark mark) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.MARK_ID.name(), mark.getId()); - result.put(FieldNames.MARK_COLOR.name(), mark.getColor()); - result.put(FieldNames.MARK_NAME.name(), mark.getName()); - result.put(FieldNames.MARK_PATTERN.name(), mark.getPattern()); - result.put(FieldNames.MARK_SHAPE.name(), mark.getShape()); - result.put(FieldNames.MARK_TYPE.name(), mark.getType() == null ? null : mark.getType().name()); - return result; - } - - private String getPassingInstructions(PassingInstruction passingInstructions) { - final String passing; - if (passingInstructions != null) { - passing = passingInstructions.name(); - } else { - passing = null; - } - return passing; - } - @Override - public void storeCompetitor(Competitor competitor) { - DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); - JSONObject json = competitorSerializer.serialize(competitor); - DBObject query = (DBObject) JSON.parse(CompetitorJsonSerializer.getCompetitorIdQuery(competitor).toString()); - DBObject entry = (DBObject) JSON.parse(json.toString()); - collection.update(query, entry, /* upsrt */true, /* multi */false, WriteConcern.SAFE); - } - - @Override - public void removeAllCompetitors() { - logger.info("Removing all persistent competitor info"); - DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); - collection.drop(); - } - - @Override - public void removeCompetitor(Competitor competitor) { - logger.info("Removing persistent competitor info for competitor "+competitor.getName()+" with ID "+competitor.getId()); - DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); - DBObject query = (DBObject) JSON.parse(CompetitorJsonSerializer.getCompetitorIdQuery(competitor).toString()); - collection.remove(query, WriteConcern.SAFE); - } - - @Override - public void storeDeviceConfiguration(DeviceConfigurationMatcher matcher, DeviceConfiguration configuration) { - DBCollection configurationsCollections = database.getCollection(CollectionNames.CONFIGURATIONS.name()); - - DBObject query = new BasicDBObject(); - query.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); - - DBObject entryObject = new BasicDBObject(); - entryObject.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); - entryObject.put(FieldNames.CONFIGURATION_MATCHER.name(), createDeviceConfigurationMatcherObject(matcher)); - entryObject.put(FieldNames.CONFIGURATION_CONFIG.name(), createDeviceConfigurationObject(configuration)); - - configurationsCollections.update(query, entryObject, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); - } - - private DBObject createDeviceConfigurationMatcherObject(DeviceConfigurationMatcher matcher) { - DBObject matcherObject = new BasicDBObject(); - matcherObject.put(FieldNames.CONFIGURATION_MATCHER_TYPE.name(), matcher.getMatcherType().name()); - if (matcher instanceof DeviceConfigurationMatcherSingle) { - BasicDBList client = new BasicDBList(); - client.add(((DeviceConfigurationMatcherSingle)matcher).getClientIdentifier()); - matcherObject.put(FieldNames.CONFIGURATION_MATCHER_CLIENTS.name(), client); - } else if (matcher instanceof DeviceConfigurationMatcherMulti) { - BasicDBList clients = new BasicDBList(); - Util.addAll(((DeviceConfigurationMatcherMulti)matcher).getClientIdentifiers(), clients); - matcherObject.put(FieldNames.CONFIGURATION_MATCHER_CLIENTS.name(), clients); - } - return matcherObject; - } - - private DBObject createDeviceConfigurationObject(DeviceConfiguration configuration) { - JsonSerializer serializer = DeviceConfigurationJsonSerializer.create(); - JSONObject json = serializer.serialize(configuration); - DBObject entry = (DBObject) JSON.parse(json.toString()); - return entry; - } - - @Override - public void removeDeviceConfiguration(DeviceConfigurationMatcher matcher) { - DBCollection configurationsCollections = database.getCollection(CollectionNames.CONFIGURATIONS.name()); - DBObject query = new BasicDBObject(); - query.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); - configurationsCollections.remove(query); - } - - public static DBObject storeDeviceId( - TypeBasedServiceFinder deviceIdentifierServiceFinder, DeviceIdentifier device) - throws TransformationException, NoCorrespondingServiceRegisteredException { - String type = device.getIdentifierType(); - DeviceIdentifierMongoHandler handler = deviceIdentifierServiceFinder.findService(type); - com.sap.sse.common.Util.Pair pair = handler.serialize(device); - type = pair.getA(); - Object deviceTypeSpecificId = pair.getB(); - return new BasicDBObjectBuilder() - .add(FieldNames.DEVICE_TYPE.name(), type) - .add(FieldNames.DEVICE_TYPE_SPECIFIC_ID.name(), deviceTypeSpecificId) - .add(FieldNames.DEVICE_STRING_REPRESENTATION.name(), device.getStringRepresentation()).get(); - } - - void storeRaceLogEventEvent(DBObject eventEntry) { - getRaceLogCollection().insert(eventEntry); - } - - @Override - public void removeRaceLog(RaceLogIdentifier identifier) { - DBObject query = new BasicDBObject(); - storeRaceLogIdentifier(identifier, query); - getRaceLogCollection().remove(query); - } - - @Override - public void removeRegattaLog(RegattaLikeIdentifier identifier) { - DBObject query = new BasicDBObject(); - addRegattaLikeIdentifier(identifier, query); - getRegattaLogCollection().remove(query); - } - - @Override - public void storeResultUrl(String resultProviderName, URL url) { - DBCollection resultUrlsCollection = database.getCollection(CollectionNames.RESULT_URLS.name()); - DBObject query = new BasicDBObject(FieldNames.RESULT_PROVIDERNAME.name(), resultProviderName); - DBObject entry = new BasicDBObject(FieldNames.RESULT_PROVIDERNAME.name(), resultProviderName); - entry.put(FieldNames.RESULT_URL.name(), url.toString()); - resultUrlsCollection.update(query, entry, /* upsrt */true, /* multi */false, WriteConcern.SAFE); - } - - @Override - public void removeResultUrl(String resultProviderName, URL url) { - DBCollection resultUrlsCollection = database.getCollection(CollectionNames.RESULT_URLS.name()); - DBObject query = new BasicDBObjectBuilder().add(FieldNames.RESULT_PROVIDERNAME.name(), resultProviderName) - .add(FieldNames.RESULT_URL.name(), url.toString()).get(); - resultUrlsCollection.remove(query); - } - - public DBCollection getRegattaLogCollection() { - DBCollection result = database.getCollection(CollectionNames.REGATTA_LOGS.name()); - DBObject index = new BasicDBObject(FieldNames.REGATTA_LOG_IDENTIFIER_TYPE.name(), null); - index.put(FieldNames.REGATTA_LOG_IDENTIFIER_NAME.name(), null); - result.createIndex(index); - return result; - } - - private DBObject createBasicRegattaLogEventDBObject(RegattaLogEvent event) { - DBObject result = new BasicDBObject(); - storeTimed(event, result); - storeTimePoint(event.getCreatedAt(), result, FieldNames.REGATTA_LOG_EVENT_CREATED_AT); - result.put(FieldNames.REGATTA_LOG_EVENT_ID.name(), event.getId()); - result.put(FieldNames.REGATTA_LOG_EVENT_AUTHOR_NAME.name(), event.getAuthor().getName()); - result.put(FieldNames.REGATTA_LOG_EVENT_AUTHOR_PRIORITY.name(), event.getAuthor().getPriority()); - return result; - } - - private void addRegattaLikeIdentifier(RegattaLikeIdentifier regattaLikeId, DBObject toObject) { - toObject.put(FieldNames.REGATTA_LOG_IDENTIFIER_TYPE.name(), regattaLikeId.getIdentifierType()); - toObject.put(FieldNames.REGATTA_LOG_IDENTIFIER_NAME.name(), regattaLikeId.getName()); - } - - private void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, DBObject innerObject) { - DBObject result = new BasicDBObject(FieldNames.REGATTA_LOG_EVENT.name(), innerObject); - addRegattaLikeIdentifier(regattaLikeId, result); - getRegattaLogCollection().insert(result); - } - - public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogDeviceCompetitorMappingEvent event) { - DBObject result = createBasicRegattaLogEventDBObject(event); - result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogDeviceCompetitorMappingEvent.class.getSimpleName()); - storeDeviceMappingEvent(event, result, FieldNames.REGATTA_LOG_FROM, FieldNames.REGATTA_LOG_TO); - result.put(FieldNames.COMPETITOR_ID.name(), event.getMappedTo().getId()); - storeRegattaLogEvent(regattaLikeId, result); - } - - public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogDeviceMarkMappingEvent event) { - DBObject result = createBasicRegattaLogEventDBObject(event); - result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogDeviceMarkMappingEvent.class.getSimpleName()); - storeDeviceMappingEvent(event, result, FieldNames.REGATTA_LOG_FROM, FieldNames.REGATTA_LOG_TO); - result.put(FieldNames.MARK.name(), storeMark(event.getMappedTo())); - storeRegattaLogEvent(regattaLikeId, result); - } - - public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogRevokeEvent event) { - DBObject result = createBasicRegattaLogEventDBObject(event); - result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogRevokeEvent.class.getSimpleName()); - result.put(FieldNames.REGATTA_LOG_REVOKED_EVENT_ID.name(), event.getRevokedEventId()); - result.put(FieldNames.REGATTA_LOG_REVOKED_EVENT_TYPE.name(), event.getRevokedEventType()); - result.put(FieldNames.REGATTA_LOG_REVOKED_EVENT_SHORT_INFO.name(), event.getRevokedEventShortInfo()); - result.put(FieldNames.REGATTA_LOG_REVOKED_REASON.name(), event.getReason()); - storeRegattaLogEvent(regattaLikeId, result); - } - - public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogRegisterCompetitorEvent event) { - DBObject result = createBasicRegattaLogEventDBObject(event); - result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogRegisterCompetitorEvent.class.getSimpleName()); - result.put(FieldNames.REGATTA_LOG_COMPETITOR_ID.name(), event.getCompetitor().getId()); - storeRegattaLogEvent(regattaLikeId, result); - } - - public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogCloseOpenEndedDeviceMappingEvent event) { - DBObject result = createBasicRegattaLogEventDBObject(event); - result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogCloseOpenEndedDeviceMappingEvent.class.getSimpleName()); - result.put(FieldNames.REGATTA_LOG_DEVICE_MAPPING_EVENT_ID.name(), event.getDeviceMappingEventId()); - storeTimePoint(event.getClosingTimePoint(), result, FieldNames.REGATTA_LOG_CLOSING_TIMEPOINT); - storeRegattaLogEvent(regattaLikeId, result); - } - - public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogSetCompetitorTimeOnTimeFactorEvent event) { - DBObject result = createBasicRegattaLogEventDBObject(event); - result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogCloseOpenEndedDeviceMappingEvent.class.getSimpleName()); - result.put(FieldNames.REGATTA_LOG_COMPETITOR_ID.name(), event.getCompetitor().getId()); - result.put(FieldNames.REGATTA_LOG_TIME_ON_TIME_FACTOR.name(), event.getTimeOnTimeFactor()); - } - - public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogDependentStartTimeEvent event) { - BasicDBObject result = new BasicDBObject(); - storeRaceLogIdentifier(raceLogIdentifier, result); - result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDependentStartTimeEvent(event)); - return result; - } - - public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogSetCompetitorTimeOnDistanceAllowancePerNauticalMileEvent event) { - DBObject result = createBasicRegattaLogEventDBObject(event); - result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogCloseOpenEndedDeviceMappingEvent.class.getSimpleName()); - result.put(FieldNames.REGATTA_LOG_COMPETITOR_ID.name(), event.getCompetitor().getId()); - result.put(FieldNames.REGATTA_LOG_TIME_ON_DISTANCE_SECONDS_ALLOWANCE_PER_NAUTICAL_MILE.name(), event.getTimeOnDistanceAllowancePerNauticalMile().asSeconds()); - storeRegattaLogEvent(regattaLikeId, result); - } - - private DBObject createImageObject(ImageDescriptor image) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.IMAGE_URL.name(), image.getURL().toString()); - result.put(FieldNames.IMAGE_LOCALE.name(), image.getLocale() != null ? image.getLocale().toLanguageTag() : null); - result.put(FieldNames.IMAGE_TITLE.name(), image.getTitle()); - result.put(FieldNames.IMAGE_SUBTITLE.name(), image.getSubtitle()); - result.put(FieldNames.IMAGE_COPYRIGHT.name(), image.getCopyright()); - result.put(FieldNames.IMAGE_WIDTH_IN_PX.name(), image.getWidthInPx()); - result.put(FieldNames.IMAGE_HEIGHT_IN_PX.name(), image.getHeightInPx()); - storeTimePoint(image.getCreatedAtDate(), result, FieldNames.IMAGE_CREATEDATDATE); - BasicDBList tags = new BasicDBList(); - for (String tag : image.getTags()) { - tags.add(tag); - } - result.put(FieldNames.IMAGE_TAGS.name(), tags); - return result; - } - - private DBObject createVideoObject(VideoDescriptor video) { - DBObject result = new BasicDBObject(); - result.put(FieldNames.VIDEO_URL.name(), video.getURL().toString()); - result.put(FieldNames.VIDEO_LOCALE.name(), video.getLocale() != null ? video.getLocale().toLanguageTag() : null); - result.put(FieldNames.VIDEO_THUMBNAIL_URL.name(), video.getThumbnailURL() != null ? video.getThumbnailURL().toString() : null); - result.put(FieldNames.VIDEO_TITLE.name(), video.getTitle()); - result.put(FieldNames.VIDEO_SUBTITLE.name(), video.getSubtitle()); - result.put(FieldNames.VIDEO_MIMETYPE.name(), video.getMimeType() != null ? video.getMimeType().name() : null); - result.put(FieldNames.VIDEO_COPYRIGHT.name(), video.getCopyright()); - result.put(FieldNames.VIDEO_LENGTH_IN_SECONDS.name(), video.getLengthInSeconds()); - storeTimePoint(video.getCreatedAtDate(), result, FieldNames.VIDEO_CREATEDATDATE); - BasicDBList tags = new BasicDBList(); - for (String tag : video.getTags()) { - tags.add(tag); - } - result.put(FieldNames.VIDEO_TAGS.name(), tags); - return result; - } -} +package com.sap.sailing.domain.persistence.impl; + +import java.io.Serializable; +import java.net.URL; +import java.util.List; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.bson.types.ObjectId; +import org.json.simple.JSONObject; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.BasicDBObjectBuilder; +import com.mongodb.DB; +import com.mongodb.DBCollection; +import com.mongodb.DBObject; +import com.mongodb.WriteConcern; +import com.mongodb.util.JSON; +import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; +import com.sap.sailing.domain.abstractlog.race.RaceLogCourseAreaChangedEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogCourseDesignChangedEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogDependentStartTimeEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogEndOfTrackingEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningConfirmedEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogFinishPositioningListChangedEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogFixedMarkPassingEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogFlagEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogGateLineOpeningTimeEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogPassChangeEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogPathfinderEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogProtestStartTimeEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogRaceStatusEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogRevokeEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogStartOfTrackingEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogSuppressedMarkPassingsEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogWindFixEvent; +import com.sap.sailing.domain.abstractlog.race.scoring.RaceLogAdditionalScoringInformationEvent; +import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogCloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDefineMarkEvent; +import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDenoteForTrackingEvent; +import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDeviceCompetitorMappingEvent; +import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogDeviceMarkMappingEvent; +import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogRegisterCompetitorEvent; +import com.sap.sailing.domain.abstractlog.race.tracking.RaceLogStartTrackingEvent; +import com.sap.sailing.domain.abstractlog.regatta.RegattaLogEvent; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogCloseOpenEndedDeviceMappingEvent; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceCompetitorMappingEvent; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogDeviceMarkMappingEvent; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogRegisterCompetitorEvent; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogRevokeEvent; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogSetCompetitorTimeOnDistanceAllowancePerNauticalMileEvent; +import com.sap.sailing.domain.abstractlog.regatta.events.RegattaLogSetCompetitorTimeOnTimeFactorEvent; +import com.sap.sailing.domain.abstractlog.shared.events.DeviceMappingEvent; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.ControlPoint; +import com.sap.sailing.domain.base.ControlPointWithTwoMarks; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.CourseBase; +import com.sap.sailing.domain.base.Event; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.RemoteSailingServerReference; +import com.sap.sailing.domain.base.SailingServerConfiguration; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.Venue; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.configuration.DeviceConfiguration; +import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; +import com.sap.sailing.domain.base.configuration.RegattaConfiguration; +import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationMatcherMulti; +import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationMatcherSingle; +import com.sap.sailing.domain.base.impl.FleetImpl; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.Positioned; +import com.sap.sailing.domain.common.RaceIdentifier; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.racelog.tracking.TransformationException; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.racelog.tracking.DeviceIdentifierMongoHandler; +import com.sap.sailing.domain.persistence.racelog.tracking.impl.PlaceHolderDeviceIdentifierMongoHandler; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.racelogtracking.DeviceIdentifier; +import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.TrackedRegatta; +import com.sap.sailing.domain.tracking.WindTrack; +import com.sap.sailing.server.gateway.serialization.JsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.CompetitorJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.DeviceConfigurationJsonSerializer; +import com.sap.sailing.server.gateway.serialization.impl.RegattaConfigurationJsonSerializer; +import com.sap.sse.common.Duration; +import com.sap.sse.common.NoCorrespondingServiceRegisteredException; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.TimeRange; +import com.sap.sse.common.Timed; +import com.sap.sse.common.TypeBasedServiceFinder; +import com.sap.sse.common.TypeBasedServiceFinderFactory; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.shared.media.ImageDescriptor; +import com.sap.sse.shared.media.VideoDescriptor; + +public class MongoObjectFactoryImpl implements MongoObjectFactory { + private static Logger logger = Logger.getLogger(MongoObjectFactoryImpl.class.getName()); + private final DB database; + private final CompetitorJsonSerializer competitorSerializer = CompetitorJsonSerializer.create(); + private final TypeBasedServiceFinder deviceIdentifierServiceFinder; + + /** + * Uses null for the device type service finder and hence will be unable to store device identifiers. + * Use this constructor only for testing purposes or in cases where there will happen absolutely no access to + * {@link DeviceIdentifier} objects. + */ + public MongoObjectFactoryImpl(DB database) { + this(database, /* deviceTypeServiceFinder */ null); + } + + public MongoObjectFactoryImpl(DB database, TypeBasedServiceFinderFactory serviceFinderFactory) { + this.database = database; + if (serviceFinderFactory != null) { + this.deviceIdentifierServiceFinder = serviceFinderFactory.createServiceFinder(DeviceIdentifierMongoHandler.class); + this.deviceIdentifierServiceFinder.setFallbackService(new PlaceHolderDeviceIdentifierMongoHandler()); + } else { + this.deviceIdentifierServiceFinder = null; + } + } + + @Override + public DB getDatabase() { + return database; + } + + public DBObject storeWind(Wind wind) { + DBObject result = new BasicDBObject(); + storePositioned(wind, result); + storeTimed(wind, result); + storeSpeedWithBearing(wind, result); + return result; + } + + public static void storeTimePoint(TimePoint timePoint, DBObject result, String fieldName) { + if (timePoint != null) { + result.put(fieldName, timePoint.asMillis()); + } + } + + public static void storeTimePoint(TimePoint timePoint, DBObject result, FieldNames field) { + storeTimePoint(timePoint, result, field.name()); + } + + public static void storeTimeRange(TimeRange timeRange, DBObject result, FieldNames field) { + if (timeRange != null) { + DBObject timeRangeObj = new BasicDBObject(); + storeTimePoint(timeRange.from(), timeRangeObj, FieldNames.FROM_MILLIS); + storeTimePoint(timeRange.to(), timeRangeObj, FieldNames.TO_MILLIS); + result.put(field.name(), timeRangeObj); + } + } + + public void storeTimed(Timed timed, DBObject result) { + if (timed.getTimePoint() != null) { + storeTimePoint(timed.getTimePoint(), result, FieldNames.TIME_AS_MILLIS); + } + } + + public void storeSpeedWithBearing(SpeedWithBearing speedWithBearing, DBObject result) { + storeSpeed(speedWithBearing, result); + storeBearing(speedWithBearing.getBearing(), result); + + } + + public void storeBearing(Bearing bearing, DBObject result) { + result.put(FieldNames.DEGREE_BEARING.name(), bearing.getDegrees()); + } + + public void storeSpeed(Speed speed, DBObject result) { + result.put(FieldNames.KNOT_SPEED.name(), speed.getKnots()); + } + + public void storePositioned(Positioned positioned, DBObject result) { + if (positioned.getPosition() != null) { + result.put(FieldNames.LAT_DEG.name(), positioned.getPosition().getLatDeg()); + result.put(FieldNames.LNG_DEG.name(), positioned.getPosition().getLngDeg()); + } + } + + @Override + public void addWindTrackDumper(TrackedRegatta trackedRegatta, TrackedRace trackedRace, WindSource windSource) { + WindTrack windTrack = trackedRace.getOrCreateWindTrack(windSource); + windTrack.addListener(new MongoWindListener(trackedRace, trackedRegatta.getRegatta().getName(), windSource, this, database)); + } + + public DBCollection getWindTrackCollection() { + DBCollection result = database.getCollection(CollectionNames.WIND_TRACKS.name()); + result.createIndex(new BasicDBObject(FieldNames.REGATTA_NAME.name(), null)); + return result; + } + + public DBCollection getGPSFixCollection() { + DBCollection gpsFixCollection = database.getCollection(CollectionNames.GPS_FIXES.name()); + DBObject index = new BasicDBObject(); + index.put(FieldNames.DEVICE_ID.name(), null); + index.put(FieldNames.TIME_AS_MILLIS.name(), null); + gpsFixCollection.createIndex(index); + return gpsFixCollection; + } + + public DBCollection getGPSFixMetadataCollection() { + DBCollection collection = database.getCollection(CollectionNames.GPS_FIXES_METADATA.name()); + DBObject index = new BasicDBObject(); + index.put(FieldNames.DEVICE_ID.name(), null); + collection.createIndex(index); + return collection; + } + + /** + * @param regattaName + * the regatta name is stored only for human readability purposes because a time stamp may be a bit unhandy for + * identifying where the wind fix was collected + */ + public DBObject storeWindTrackEntry(RaceDefinition race, String regattaName, WindSource windSource, Wind wind) { + BasicDBObject result = new BasicDBObject(); + result.put(FieldNames.RACE_ID.name(), race.getId()); + result.put(FieldNames.REGATTA_NAME.name(), regattaName); + result.put(FieldNames.WIND_SOURCE_NAME.name(), windSource.name()); + if (windSource.getId() != null) { + result.put(FieldNames.WIND_SOURCE_ID.name(), windSource.getId()); + } + result.put(FieldNames.WIND.name(), storeWind(wind)); + return result; + } + + private void storeRaceIdentifiers(RaceColumn raceColumn, DBObject dbObject) { + BasicDBObject raceIdentifiersPerFleet = new BasicDBObject(); + for (Fleet fleet : raceColumn.getFleets()) { + RaceIdentifier raceIdentifier = raceColumn.getRaceIdentifier(fleet); + if (raceIdentifier != null) { + DBObject raceIdentifierForFleet = new BasicDBObject(); + storeRaceIdentifier(raceIdentifierForFleet, raceIdentifier); + raceIdentifiersPerFleet.put(MongoUtils.escapeDollarAndDot(fleet.getName()), raceIdentifierForFleet); + } + } + dbObject.put(FieldNames.RACE_IDENTIFIERS.name(), raceIdentifiersPerFleet); + } + + private void storeRaceIdentifier(DBObject dbObject, RaceIdentifier raceIdentifier) { + if (raceIdentifier != null) { + dbObject.put(FieldNames.EVENT_NAME.name(), raceIdentifier.getRegattaName()); + dbObject.put(FieldNames.RACE_NAME.name(), raceIdentifier.getRaceName()); + } + } + + @Override + public void storeLeaderboard(Leaderboard leaderboard) { + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + try { + leaderboardCollection.createIndex(new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), 1)); + } catch (NullPointerException npe) { + // sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an NPE + logger.log(Level.SEVERE, "storeLeaderboard", npe); + } + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); + BasicDBObject dbLeaderboard = new BasicDBObject(); + dbLeaderboard.put(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); + if (leaderboard.getDisplayName() != null) { + dbLeaderboard.put(FieldNames.LEADERBOARD_DISPLAY_NAME.name(), leaderboard.getDisplayName()); + } + BasicDBList dbSuppressedCompetitorIds = new BasicDBList(); + for (Competitor suppressedCompetitor : leaderboard.getSuppressedCompetitors()) { + dbSuppressedCompetitorIds.add(suppressedCompetitor.getId()); + } + dbLeaderboard.put(FieldNames.LEADERBOARD_SUPPRESSED_COMPETITOR_IDS.name(), dbSuppressedCompetitorIds); + if (leaderboard instanceof FlexibleLeaderboard) { + storeFlexibleLeaderboard((FlexibleLeaderboard) leaderboard, dbLeaderboard); + } else if (leaderboard instanceof RegattaLeaderboard) { + storeRegattaLeaderboard((RegattaLeaderboard) leaderboard, dbLeaderboard); + } else { + // at least store the scoring scheme + dbLeaderboard.put(FieldNames.SCORING_SCHEME_TYPE.name(), leaderboard.getScoringScheme().getType().name()); + } + if (leaderboard.getDefaultCourseArea() != null) { + dbLeaderboard.put(FieldNames.COURSE_AREA_ID.name(), leaderboard.getDefaultCourseArea().getId().toString()); + } else { + dbLeaderboard.put(FieldNames.COURSE_AREA_ID.name(), null); + } + storeColumnFactors(leaderboard, dbLeaderboard); + storeLeaderboardCorrectionsAndDiscards(leaderboard, dbLeaderboard); + leaderboardCollection.update(query, dbLeaderboard, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } + + private void storeColumnFactors(Leaderboard leaderboard, BasicDBObject dbLeaderboard) { + DBObject raceColumnFactors = new BasicDBObject(); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + Double explicitFactor = raceColumn.getExplicitFactor(); + if (explicitFactor != null) { + raceColumnFactors.put(MongoUtils.escapeDollarAndDot(raceColumn.getName()), explicitFactor); + } + } + dbLeaderboard.put(FieldNames.LEADERBOARD_COLUMN_FACTORS.name(), raceColumnFactors); + } + + private void storeRegattaLeaderboard(RegattaLeaderboard leaderboard, DBObject dbLeaderboard) { + dbLeaderboard.put(FieldNames.REGATTA_NAME.name(), leaderboard.getRegatta().getName()); + } + + private void storeFlexibleLeaderboard(FlexibleLeaderboard leaderboard, BasicDBObject dbLeaderboard) { + BasicDBList dbRaceColumns = new BasicDBList(); + dbLeaderboard.put(FieldNames.SCORING_SCHEME_TYPE.name(), leaderboard.getScoringScheme().getType().name()); + dbLeaderboard.put(FieldNames.LEADERBOARD_COLUMNS.name(), dbRaceColumns); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + BasicDBObject dbRaceColumn = storeRaceColumn(raceColumn); + dbRaceColumns.add(dbRaceColumn); + } + } + + private void storeLeaderboardCorrectionsAndDiscards(Leaderboard leaderboard, BasicDBObject dbLeaderboard) { + if (leaderboard.hasCarriedPoints()) { + BasicDBList dbCarriedPoints = new BasicDBList(); + dbLeaderboard.put(FieldNames.LEADERBOARD_CARRIED_POINTS_BY_ID.name(), dbCarriedPoints); + for (Entry competitorWithCarriedPoints : leaderboard + .getCompetitorsForWhichThereAreCarriedPoints().entrySet()) { + double carriedPoints = competitorWithCarriedPoints.getValue(); + Competitor competitor = competitorWithCarriedPoints.getKey(); + DBObject dbCarriedPointsForCompetitor = new BasicDBObject(); + dbCarriedPointsForCompetitor.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); + dbCarriedPointsForCompetitor.put(FieldNames.LEADERBOARD_CARRIED_POINTS.name(), carriedPoints); + dbCarriedPoints.add(dbCarriedPointsForCompetitor); + } + } + BasicDBObject dbScoreCorrections = new BasicDBObject(); + storeScoreCorrections(leaderboard, dbScoreCorrections); + dbLeaderboard.put(FieldNames.LEADERBOARD_SCORE_CORRECTIONS.name(), dbScoreCorrections); + final ResultDiscardingRule resultDiscardingRule = leaderboard.getResultDiscardingRule(); + storeResultDiscardingRule(dbLeaderboard, resultDiscardingRule, FieldNames.LEADERBOARD_DISCARDING_THRESHOLDS); + BasicDBList competitorDisplayNames = new BasicDBList(); + for (Competitor competitor : leaderboard.getCompetitors()) { + String displayNameForCompetitor = leaderboard.getDisplayName(competitor); + if (displayNameForCompetitor != null) { + DBObject dbDisplayName = new BasicDBObject(); + dbDisplayName.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); + dbDisplayName.put(FieldNames.COMPETITOR_DISPLAY_NAME.name(), displayNameForCompetitor); + competitorDisplayNames.add(dbDisplayName); + } + } + dbLeaderboard.put(FieldNames.LEADERBOARD_COMPETITOR_DISPLAY_NAMES.name(), competitorDisplayNames); + } + + /** + * Stores the result discarding rule to dbObject's field identified by field if the result discarding + * rule is not null and is of type {@link ThresholdBasedResultDiscardingRule}. Otherwise, it is assumed that the + * result discarding rule is otherwise implicitly obtained, e.g., from a definition of a regatta with its series, stored elsewhere. + */ + private void storeResultDiscardingRule(DBObject dbObject, + final ResultDiscardingRule resultDiscardingRule, FieldNames field) { + if (resultDiscardingRule != null && resultDiscardingRule instanceof ThresholdBasedResultDiscardingRule) { + BasicDBList dbResultDiscardingThresholds = new BasicDBList(); + for (int threshold : ((ThresholdBasedResultDiscardingRule) resultDiscardingRule).getDiscardIndexResultsStartingWithHowManyRaces()) { + dbResultDiscardingThresholds.add(threshold); + } + dbObject.put(field.name(), dbResultDiscardingThresholds); + } + } + + private BasicDBObject storeRaceColumn(RaceColumn raceColumn) { + BasicDBObject dbRaceColumn = new BasicDBObject(); + dbRaceColumn.put(FieldNames.LEADERBOARD_COLUMN_NAME.name(), raceColumn.getName()); + dbRaceColumn.put(FieldNames.LEADERBOARD_IS_MEDAL_RACE_COLUMN.name(), raceColumn.isMedalRace()); + storeRaceIdentifiers(raceColumn, dbRaceColumn); + return dbRaceColumn; + } + + private void storeScoreCorrections(Leaderboard leaderboard, BasicDBObject dbScoreCorrections) { + TimePoint now = MillisecondsTimePoint.now(); + SettableScoreCorrection scoreCorrection = leaderboard.getScoreCorrection(); + for (RaceColumn raceColumn : scoreCorrection.getRaceColumnsThatHaveCorrections()) { + BasicDBList dbCorrectionForRace = new BasicDBList(); + for (Competitor competitor : scoreCorrection.getCompetitorsThatHaveCorrectionsIn(raceColumn)) { + // TODO bug 655: make score corrections time dependent + if (scoreCorrection.isScoreCorrected(competitor, raceColumn, now)) { + BasicDBObject dbCorrectionForCompetitor = new BasicDBObject(); + dbCorrectionForCompetitor.put(FieldNames.COMPETITOR_ID.name(), competitor.getId()); + MaxPointsReason maxPointsReason = scoreCorrection.getMaxPointsReason(competitor, raceColumn, now); + if (maxPointsReason != MaxPointsReason.NONE) { + dbCorrectionForCompetitor.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name(), + maxPointsReason.name()); + } + Double explicitScoreCorrection = scoreCorrection + .getExplicitScoreCorrection(competitor, raceColumn); + if (explicitScoreCorrection != null) { + dbCorrectionForCompetitor.put(FieldNames.LEADERBOARD_CORRECTED_SCORE.name(), + explicitScoreCorrection); + } + dbCorrectionForRace.add(dbCorrectionForCompetitor); + } + } + if (!dbCorrectionForRace.isEmpty()) { + // using the column name as the key for the score corrections requires re-writing the score corrections + // of a meta-leaderboard if the name of one of its leaderboards changes + dbScoreCorrections.put(MongoUtils.escapeDollarAndDot(raceColumn.getName()), dbCorrectionForRace); + } + } + final TimePoint timePointOfLastCorrectionsValidity = scoreCorrection.getTimePointOfLastCorrectionsValidity(); + if (timePointOfLastCorrectionsValidity != null) { + dbScoreCorrections.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_TIMESTAMP.name(), timePointOfLastCorrectionsValidity.asMillis()); + } + if (scoreCorrection.getComment() != null) { + dbScoreCorrections.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_COMMENT.name(), scoreCorrection.getComment()); + } + } + + @Override + public void removeLeaderboard(String leaderboardName) { + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboardName); + leaderboardCollection.remove(query); + } + + @Override + public void renameLeaderboard(String oldName, String newName) { + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), oldName); + BasicDBObject renameUpdate = new BasicDBObject("$set", new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), newName)); + leaderboardCollection.update(query, renameUpdate, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void storeLeaderboardGroup(LeaderboardGroup leaderboardGroup) { + DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); + DBCollection leaderboardCollection = database.getCollection(CollectionNames.LEADERBOARDS.name()); + + try { + leaderboardGroupCollection.createIndex(new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), 1)); + } catch (NullPointerException npe) { + // sometimes, for reasons yet to be clarified, ensuring an index on the name field causes an NPE + logger.log(Level.SEVERE, "storeLeaderboardGroup", npe); + } + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), leaderboardGroup.getName()); + BasicDBObject dbLeaderboardGroup = new BasicDBObject(); + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_UUID.name(), leaderboardGroup.getId()); + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_NAME.name(), leaderboardGroup.getName()); + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_DESCRIPTION.name(), leaderboardGroup.getDescription()); + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_DISPLAY_NAME.name(), leaderboardGroup.getDisplayName()); + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_DISPLAY_IN_REVERSE_ORDER.name(), leaderboardGroup.isDisplayGroupsInReverseOrder()); + final Leaderboard overallLeaderboard = leaderboardGroup.getOverallLeaderboard(); + if (overallLeaderboard != null) { + BasicDBObject overallLeaderboardQuery = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), overallLeaderboard.getName()); + DBObject dbOverallLeaderboard = leaderboardCollection.findOne(overallLeaderboardQuery); + if (dbOverallLeaderboard == null) { + storeLeaderboard(overallLeaderboard); + dbOverallLeaderboard = leaderboardCollection.findOne(overallLeaderboardQuery); + } + ObjectId dbOverallLeaderboardId = (ObjectId) dbOverallLeaderboard.get("_id"); + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_OVERALL_LEADERBOARD.name(), dbOverallLeaderboardId); + } + BasicDBList dbLeaderboardIds = new BasicDBList(); + for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { + BasicDBObject leaderboardQuery = new BasicDBObject(FieldNames.LEADERBOARD_NAME.name(), leaderboard.getName()); + DBObject dbLeaderboard = leaderboardCollection.findOne(leaderboardQuery); + if (dbLeaderboard == null) { + storeLeaderboard(leaderboard); + dbLeaderboard = leaderboardCollection.findOne(leaderboardQuery); + } + ObjectId dbLeaderboardId = (ObjectId) dbLeaderboard.get("_id"); + dbLeaderboardIds.add(dbLeaderboardId); + } + dbLeaderboardGroup.put(FieldNames.LEADERBOARD_GROUP_LEADERBOARDS.name(), dbLeaderboardIds); + leaderboardGroupCollection.update(query, dbLeaderboardGroup, true, false, WriteConcern.SAFE); + } + + @Override + public void removeLeaderboardGroup(String groupName) { + DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), groupName); + leaderboardGroupCollection.remove(query); + } + + @Override + public void renameLeaderboardGroup(String oldName, String newName) { + DBCollection leaderboardGroupCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUPS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), oldName); + BasicDBObject update = new BasicDBObject("$set", new BasicDBObject(FieldNames.LEADERBOARD_GROUP_NAME.name(), newName)); + leaderboardGroupCollection.update(query, update, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void storeServerConfiguration(SailingServerConfiguration serverConfiguration) { + DBCollection serverCollection = database.getCollection(CollectionNames.SERVER_CONFIGURATION.name()); + DBObject newServerConfig = new BasicDBObject(); + newServerConfig.put(FieldNames.SERVER_IS_STANDALONE.name(), serverConfiguration.isStandaloneServer()); + DBObject currentServerConfig = serverCollection.findOne(); + if(currentServerConfig != null) { + serverCollection.update(currentServerConfig, newServerConfig, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } else { + serverCollection.save(newServerConfig); + } + } + + @Override + public void storeSailingServer(RemoteSailingServerReference server) { + DBCollection serverCollection = database.getCollection(CollectionNames.SAILING_SERVERS.name()); + serverCollection.createIndex(new BasicDBObject(FieldNames.SERVER_NAME.name(), 1)); + DBObject query = new BasicDBObject(); + query.put(FieldNames.SERVER_NAME.name(), server.getName()); + DBObject serverDBObject = new BasicDBObject(); + serverDBObject.put(FieldNames.SERVER_NAME.name(), server.getName()); + serverDBObject.put(FieldNames.SERVER_URL.name(), server.getURL().toExternalForm()); + serverCollection.update(query, serverDBObject, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void removeSailingServer(String name) { + DBCollection serverCollection = database.getCollection(CollectionNames.SAILING_SERVERS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.SERVER_NAME.name(), name); + serverCollection.remove(query); + } + + /** + * StoreEvent() uses some deprecated methods of event to keep backward compatibility. + */ + @SuppressWarnings("deprecation") + @Override + public void storeEvent(Event event) { + DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); + eventCollection.createIndex(new BasicDBObject(FieldNames.EVENT_ID.name(), 1)); + DBObject query = new BasicDBObject(); + query.put(FieldNames.EVENT_ID.name(), event.getId()); + DBObject eventDBObject = new BasicDBObject(); + eventDBObject.put(FieldNames.EVENT_NAME.name(), event.getName()); + eventDBObject.put(FieldNames.EVENT_DESCRIPTION.name(), event.getDescription()); + eventDBObject.put(FieldNames.EVENT_ID.name(), event.getId()); + eventDBObject.put(FieldNames.EVENT_LOGO_IMAGE_URL.name(), event.getLogoImageURL() != null ? event.getLogoImageURL().toString() : null); + eventDBObject.put(FieldNames.EVENT_OFFICIAL_WEBSITE_URL.name(), event.getOfficialWebsiteURL() != null ? event.getOfficialWebsiteURL().toString() : null); + eventDBObject.put(FieldNames.EVENT_SAILORS_INFO_WEBSITE_URL.name(), event.getSailorsInfoWebsiteURL() != null ? event.getSailorsInfoWebsiteURL().toString() : null); + storeTimePoint(event.getStartDate(), eventDBObject, FieldNames.EVENT_START_DATE); + storeTimePoint(event.getEndDate(), eventDBObject, FieldNames.EVENT_END_DATE); + eventDBObject.put(FieldNames.EVENT_IS_PUBLIC.name(), event.isPublic()); + DBObject venueDBObject = getVenueAsDBObject(event.getVenue()); + eventDBObject.put(FieldNames.VENUE.name(), venueDBObject); + BasicDBList imageURLs = new BasicDBList(); + for (URL imageURL : event.getImageURLs()) { + imageURLs.add(imageURL.toString()); + } + eventDBObject.put(FieldNames.EVENT_IMAGE_URLS.name(), imageURLs); + BasicDBList videoURLs = new BasicDBList(); + for (URL videoURL : event.getVideoURLs()) { + videoURLs.add(videoURL.toString()); + } + eventDBObject.put(FieldNames.EVENT_VIDEO_URLS.name(), videoURLs); + BasicDBList sponsorImageURLs = new BasicDBList(); + for (URL sponsorImageURL : event.getSponsorImageURLs()) { + sponsorImageURLs.add(sponsorImageURL.toString()); + } + eventDBObject.put(FieldNames.EVENT_SPONSOR_IMAGE_URLS.name(), sponsorImageURLs); + BasicDBList images = new BasicDBList(); + for (ImageDescriptor image : event.getImages()) { + DBObject imageObject = createImageObject(image); + images.add(imageObject); + } + eventDBObject.put(FieldNames.EVENT_IMAGES.name(), images); + BasicDBList videos = new BasicDBList(); + for (VideoDescriptor video: event.getVideos()) { + DBObject videoObject = createVideoObject(video); + videos.add(videoObject); + } + eventDBObject.put(FieldNames.EVENT_VIDEOS.name(), videos); + eventCollection.update(query, eventDBObject, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + // now store the links to the leaderboard groups + DBCollection linksCollection = database.getCollection(CollectionNames.LEADERBOARD_GROUP_LINKS_FOR_EVENTS.name()); + linksCollection.createIndex(new BasicDBObject(FieldNames.EVENT_ID.name(), 1)); + BasicDBList lgUUIDs = new BasicDBList(); + for (LeaderboardGroup lg : event.getLeaderboardGroups()) { + lgUUIDs.add(lg.getId()); + } + DBObject dbLinks = new BasicDBObject(); + dbLinks.put(FieldNames.EVENT_ID.name(), event.getId()); + dbLinks.put(FieldNames.LEADERBOARD_GROUP_UUID.name(), lgUUIDs); + linksCollection.update(query, dbLinks, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void renameEvent(Serializable id, String newName) { + DBCollection eventCollection = database.getCollection(CollectionNames.EVENTS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.EVENT_ID.name(), id); + BasicDBObject renameUpdate = new BasicDBObject("$set", new BasicDBObject(FieldNames.EVENT_NAME.name(), newName)); + eventCollection.update(query, renameUpdate, /* upsert */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void removeEvent(Serializable id) { + DBCollection eventsCollection = database.getCollection(CollectionNames.EVENTS.name()); + BasicDBObject query = new BasicDBObject(FieldNames.EVENT_ID.name(), id); + eventsCollection.remove(query); + } + + private DBObject getVenueAsDBObject(Venue venue) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.VENUE_NAME.name(), venue.getName()); + BasicDBList courseAreaList = new BasicDBList(); + result.put(FieldNames.COURSE_AREAS.name(), courseAreaList); + for (CourseArea courseArea : venue.getCourseAreas()) { + DBObject dbCourseArea = new BasicDBObject(); + courseAreaList.add(dbCourseArea); + dbCourseArea.put(FieldNames.COURSE_AREA_NAME.name(), courseArea.getName()); + dbCourseArea.put(FieldNames.COURSE_AREA_ID.name(), courseArea.getId()); + } + return result; + } + + @Override + public void storeRegatta(Regatta regatta) { + DBCollection regattasCollection = database.getCollection(CollectionNames.REGATTAS.name()); + regattasCollection.createIndex(new BasicDBObject(FieldNames.REGATTA_NAME.name(), 1)); + regattasCollection.createIndex(new BasicDBObject(FieldNames.REGATTA_ID.name(), 1)); + DBObject dbRegatta = new BasicDBObject(); + DBObject query = new BasicDBObject(FieldNames.REGATTA_NAME.name(), regatta.getName()); + dbRegatta.put(FieldNames.REGATTA_NAME.name(), regatta.getName()); + dbRegatta.put(FieldNames.REGATTA_ID.name(), regatta.getId()); + storeTimePoint(regatta.getStartDate(), dbRegatta, FieldNames.REGATTA_START_DATE); + storeTimePoint(regatta.getEndDate(), dbRegatta, FieldNames.REGATTA_END_DATE); + dbRegatta.put(FieldNames.SCORING_SCHEME_TYPE.name(), regatta.getScoringScheme().getType().name()); + if (regatta.getBoatClass() != null) { + dbRegatta.put(FieldNames.BOAT_CLASS_NAME.name(), regatta.getBoatClass().getName()); + dbRegatta.put(FieldNames.BOAT_CLASS_TYPICALLY_STARTS_UPWIND.name(), regatta.getBoatClass().typicallyStartsUpwind()); + } + dbRegatta.put(FieldNames.REGATTA_SERIES.name(), storeSeries(regatta.getSeries())); + + if (regatta.getDefaultCourseArea() != null) { + dbRegatta.put(FieldNames.COURSE_AREA_ID.name(), regatta.getDefaultCourseArea().getId().toString()); + } else { + dbRegatta.put(FieldNames.COURSE_AREA_ID.name(), null); + } + if (regatta.getRegattaConfiguration() != null) { + JsonSerializer serializer = RegattaConfigurationJsonSerializer.create(); + JSONObject json = serializer.serialize(regatta.getRegattaConfiguration()); + DBObject configurationObject = (DBObject) JSON.parse(json.toString()); + dbRegatta.put(FieldNames.REGATTA_REGATTA_CONFIGURATION.name(), configurationObject); + } + dbRegatta.put(FieldNames.REGATTA_USE_START_TIME_INFERENCE.name(), regatta.useStartTimeInference()); + dbRegatta.put(FieldNames.REGATTA_RANKING_METRIC.name(), storeRankingMetric(regatta)); + regattasCollection.update(query, dbRegatta, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } + + private DBObject storeRankingMetric(Regatta regatta) { + DBObject rankingMetricJson = new BasicDBObject(); + final String rankingMetricTypeName = regatta.getRankingMetricType().name(); + rankingMetricJson.put(FieldNames.REGATTA_RANKING_METRIC_TYPE.name(), rankingMetricTypeName); + return rankingMetricJson; + } + + @Override + public void removeRegatta(Regatta regatta) { + DBCollection regattasCollection = database.getCollection(CollectionNames.REGATTAS.name()); + DBObject query = new BasicDBObject(FieldNames.REGATTA_NAME.name(), regatta.getName()); + regattasCollection.remove(query); + } + + private BasicDBList storeSeries(Iterable series) { + BasicDBList dbSeries = new BasicDBList(); + for (Series s : series) { + dbSeries.add(storeSeries(s)); + } + return dbSeries; + } + + private DBObject storeSeries(Series s) { + DBObject dbSeries = new BasicDBObject(); + dbSeries.put(FieldNames.SERIES_NAME.name(), s.getName()); + dbSeries.put(FieldNames.SERIES_IS_MEDAL.name(), s.isMedal()); + dbSeries.put(FieldNames.SERIES_HAS_SPLIT_FLEET_CONTIGUOUS_SCORING.name(), s.hasSplitFleetContiguousScoring()); + dbSeries.put(FieldNames.SERIES_STARTS_WITH_ZERO_SCORE.name(), s.isStartsWithZeroScore()); + dbSeries.put(FieldNames.SERIES_STARTS_WITH_NON_DISCARDABLE_CARRY_FORWARD.name(), s.isFirstColumnIsNonDiscardableCarryForward()); + BasicDBList dbFleets = new BasicDBList(); + for (Fleet fleet : s.getFleets()) { + dbFleets.add(storeFleet(fleet)); + } + dbSeries.put(FieldNames.SERIES_FLEETS.name(), dbFleets); + BasicDBList dbRaceColumns = new BasicDBList(); + for (RaceColumn raceColumn : s.getRaceColumns()) { + dbRaceColumns.add(storeRaceColumn(raceColumn)); + } + dbSeries.put(FieldNames.SERIES_RACE_COLUMNS.name(), dbRaceColumns); + if (s.getResultDiscardingRule() != null) { + storeResultDiscardingRule(dbSeries, s.getResultDiscardingRule(), FieldNames.SERIES_DISCARDING_THRESHOLDS); + } + return dbSeries; + } + + private DBObject storeFleet(Fleet fleet) { + DBObject dbFleet = new BasicDBObject(FieldNames.FLEET_NAME.name(), fleet.getName()); + if (fleet instanceof FleetImpl) { + dbFleet.put(FieldNames.FLEET_ORDERING.name(), ((FleetImpl) fleet).getOrdering()); + if(fleet.getColor() != null) { + com.sap.sse.common.Util.Triple colorAsRGB = fleet.getColor().getAsRGB(); + // we save the color as a integer value representing the RGB values + int colorAsInt = (256 * 256 * colorAsRGB.getC()) + colorAsRGB.getB() * 256 + colorAsRGB.getA(); + dbFleet.put(FieldNames.FLEET_COLOR.name(), colorAsInt); + } else { + dbFleet.put(FieldNames.FLEET_COLOR.name(), null); + } + } + return dbFleet; + } + + @Override + public void storeRegattaForRaceID(String raceIDAsString, Regatta regatta) { + DBCollection regattaForRaceIDCollection = database.getCollection(CollectionNames.REGATTA_FOR_RACE_ID.name()); + DBObject query = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); + DBObject entry = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); + entry.put(FieldNames.REGATTA_NAME.name(), regatta.getName()); + regattaForRaceIDCollection.update(query, entry, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } + + @Override + public void removeRegattaForRaceID(String raceIDAsString, Regatta regatta) { + DBCollection regattaForRaceIDCollection = database.getCollection(CollectionNames.REGATTA_FOR_RACE_ID.name()); + DBObject query = new BasicDBObject(FieldNames.RACE_ID_AS_STRING.name(), raceIDAsString); + regattaForRaceIDCollection.remove(query); + } + + public DBCollection getRaceLogCollection() { + DBCollection result = database.getCollection(CollectionNames.RACE_LOGS.name()); + result.createIndex(new BasicDBObject(FieldNames.RACE_LOG_IDENTIFIER.name(), null)); + return result; + } + + private void storeRaceLogEventAuthor(DBObject dbObject, AbstractLogEventAuthor author) { + if (author != null) { + dbObject.put(FieldNames.RACE_LOG_EVENT_AUTHOR_NAME.name(), author.getName()); + dbObject.put(FieldNames.RACE_LOG_EVENT_AUTHOR_PRIORITY.name(), author.getPriority()); + } + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFlagEvent flagEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFlagEvent(flagEvent)); + return result; + } + + private void storeRaceLogIdentifier(RaceLogIdentifier raceLogIdentifier, DBObject result) { + result.put(FieldNames.RACE_LOG_IDENTIFIER.name(), TripleSerializer.serialize(raceLogIdentifier.getIdentifier())); + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartTimeEvent startTimeEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartTimeEvent(startTimeEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogPassChangeEvent passChangeEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogPassChangeEvent(passChangeEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogRaceStatusEvent raceStatusEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogRaceStatusEvent(raceStatusEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCourseAreaChangedEvent courseAreaChangedEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCourseAreaChangedEvent(courseAreaChangedEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCourseDesignChangedEvent courseDesignChangedEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCourseDesignChangedEvent(courseDesignChangedEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFinishPositioningListChangedEvent finishPositioningListChangedEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFinishPositioningListChangedEvent(finishPositioningListChangedEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFinishPositioningConfirmedEvent finishPositioningConfirmedEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFinishPositioningConfirmedEvent(finishPositioningConfirmedEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogPathfinderEvent pathfinderEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogPathfinderEvent(pathfinderEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogGateLineOpeningTimeEvent gateLineOpeningTimeEvent) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogGateLineOpeningTimeEvent(gateLineOpeningTimeEvent)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartProcedureChangedEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartProcedureChangedEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogProtestStartTimeEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogProtestStartTimeEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogWindFixEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogWindFix(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogDeviceCompetitorMappingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDeviceCompetitorMappingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogDeviceMarkMappingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDeviceMarkMappingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogDenoteForTrackingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDenoteForTrackingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartTrackingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartTrackingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogRevokeEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogRevokeEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogRegisterCompetitorEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogRegisterCompetitorEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogDefineMarkEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDefineMarkEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogCloseOpenEndedDeviceMappingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogCloseOpenEndedDeviceMappingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogEndOfTrackingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogEndOfTrackingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogStartOfTrackingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogStartOfTrackingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogAdditionalScoringInformationEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeAdditionalScoringInformation(event)); + return result; + } + + private Object storeAdditionalScoringInformation(RaceLogAdditionalScoringInformationEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogAdditionalScoringInformationEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_ADDITIONAL_SCORING_INFORMATION_TYPE.name(), event.getType().name()); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogFixedMarkPassingEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogFixedMarkPassingEvent(event)); + return result; + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogSuppressedMarkPassingsEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogSuppressedMarkPassingsEvent(event)); + return result; + } + + private Object storeRaceLogWindFix(RaceLogWindFixEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogWindFixEvent.class.getSimpleName()); + result.put(FieldNames.WIND.name(), storeWind(event.getWindFix())); + result.put(FieldNames.IS_MAGNETIC.name(), event.isMagnetic()); + return result; + } + + private Object storeRaceLogProtestStartTimeEvent(RaceLogProtestStartTimeEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogProtestStartTimeEvent.class.getSimpleName()); + storeTimePoint(event.getProtestStartTime(), result, FieldNames.RACE_LOG_PROTEST_START_TIME); + return result; + } + + private Object storeRaceLogEndOfTrackingEvent(RaceLogEndOfTrackingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogEndOfTrackingEvent.class.getSimpleName()); + return result; + } + + private Object storeRaceLogStartOfTrackingEvent(RaceLogStartOfTrackingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartOfTrackingEvent.class.getSimpleName()); + return result; + } + + private Object storeRaceLogStartProcedureChangedEvent(RaceLogStartProcedureChangedEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartProcedureChangedEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_START_PROCEDURE_TYPE.name(), event.getStartProcedureType().name()); + return result; + } + + private Object storeRaceLogPathfinderEvent(RaceLogPathfinderEvent pathfinderEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(pathfinderEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogPathfinderEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_PATHFINDER_ID.name(), pathfinderEvent.getPathfinderId()); + return result; + } + + private void storeDeviceMappingEvent(DeviceMappingEvent event, DBObject result, FieldNames fromField, FieldNames toField) { + try { + result.put(FieldNames.DEVICE_ID.name(), storeDeviceId(deviceIdentifierServiceFinder, event.getDevice())); + } catch (TransformationException | NoCorrespondingServiceRegisteredException e) { + logger.log(Level.WARNING, "Could not store device identifier for mappng event", e); + } + if (event.getFrom() != null) { + storeTimePoint(event.getFrom(), result, fromField); + } + if (event.getTo() != null) { + storeTimePoint(event.getTo(), result, toField); + } + } + + private Object storeRaceLogDeviceCompetitorMappingEvent(RaceLogDeviceCompetitorMappingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogDeviceCompetitorMappingEvent.class.getSimpleName()); + storeDeviceMappingEvent(event, result, FieldNames.RACE_LOG_FROM, FieldNames.RACE_LOG_TO); + result.put(FieldNames.COMPETITOR_ID.name(), event.getMappedTo().getId()); + return result; + } + + private Object storeRaceLogDeviceMarkMappingEvent(RaceLogDeviceMarkMappingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogDeviceMarkMappingEvent.class.getSimpleName()); + storeDeviceMappingEvent(event, result, FieldNames.RACE_LOG_FROM, FieldNames.RACE_LOG_TO); + result.put(FieldNames.MARK.name(), storeMark(event.getMappedTo())); + return result; + } + + private Object storeRaceLogDenoteForTrackingEvent(RaceLogDenoteForTrackingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogDenoteForTrackingEvent.class.getSimpleName()); + result.put(FieldNames.RACE_NAME.name(), event.getRaceName()); + result.put(FieldNames.BOAT_CLASS_NAME.name(), event.getBoatClass().getName()); + result.put(FieldNames.RACE_ID.name(), event.getRaceId()); + return result; + } + + private Object storeRaceLogStartTrackingEvent(RaceLogStartTrackingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartTrackingEvent.class.getSimpleName()); + return result; + } + + private Object storeRaceLogRevokeEvent(RaceLogRevokeEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogRevokeEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_REVOKED_EVENT_ID.name(), event.getRevokedEventId()); + result.put(FieldNames.RACE_LOG_REVOKED_EVENT_TYPE.name(), event.getRevokedEventType()); + result.put(FieldNames.RACE_LOG_REVOKED_EVENT_SHORT_INFO.name(), event.getRevokedEventShortInfo()); + result.put(FieldNames.RACE_LOG_REVOKED_REASON.name(), event.getReason()); + return result; + } + + private Object storeRaceLogRegisterCompetitorEvent(RaceLogRegisterCompetitorEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogRegisterCompetitorEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_COMPETITOR_ID.name(), event.getCompetitor().getId()); + return result; + } + + private Object storeRaceLogDefineMarkEvent(RaceLogDefineMarkEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogDefineMarkEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_MARK.name(), storeMark(event.getMark())); + return result; + } + + private Object storeRaceLogCloseOpenEndedDeviceMappingEvent(RaceLogCloseOpenEndedDeviceMappingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogCloseOpenEndedDeviceMappingEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_DEVICE_MAPPING_EVENT_ID.name(), event.getDeviceMappingEventId()); + storeTimePoint(event.getClosingTimePoint(), result, FieldNames.RACE_LOG_CLOSING_TIMEPOINT); + return result; + } + + private Object storeRaceLogFixedMarkPassingEvent(RaceLogFixedMarkPassingEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFixedMarkPassingEvent.class.getSimpleName()); + result.put(FieldNames.INDEX_OF_PASSED_WAYPOINT.name(), event.getZeroBasedIndexOfPassedWaypoint()); + result.put(FieldNames.TIMEPOINT_OF_FIXED_MARKPASSING.name(), event.getTimePointOfFixedPassing().asMillis()); + return result; + } + + private Object storeRaceLogSuppressedMarkPassingsEvent(RaceLogSuppressedMarkPassingsEvent event) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(event, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogSuppressedMarkPassingsEvent.class.getSimpleName()); + result.put(FieldNames.INDEX_OF_FIRST_SUPPRESSED_WAYPOINT.name(), event.getZeroBasedIndexOfFirstSuppressedWaypoint()); + return result; + } + + public DBObject storeRaceLogFlagEvent(RaceLogFlagEvent flagEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(flagEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFlagEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_EVENT_FLAG_UPPER.name(), flagEvent.getUpperFlag().name()); + result.put(FieldNames.RACE_LOG_EVENT_FLAG_LOWER.name(), flagEvent.getLowerFlag().name()); + result.put(FieldNames.RACE_LOG_EVENT_FLAG_DISPLAYED.name(), String.valueOf(flagEvent.isDisplayed())); + return result; + } + + private DBObject storeRaceLogStartTimeEvent(RaceLogStartTimeEvent startTimeEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(startTimeEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogStartTimeEvent.class.getSimpleName()); + storeTimePoint(startTimeEvent.getStartTime(), result, FieldNames.RACE_LOG_EVENT_START_TIME); + result.put(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name(), startTimeEvent.getNextStatus().name()); + return result; + } + + private void storeRaceLogEventProperties(RaceLogEvent event, DBObject result) { + // for compatibility reasons we reuse the field name of Timed + storeTimePoint(event.getLogicalTimePoint(), result, FieldNames.TIME_AS_MILLIS); + storeTimePoint(event.getCreatedAt(), result, FieldNames.RACE_LOG_EVENT_CREATED_AT); + result.put(FieldNames.RACE_LOG_EVENT_ID.name(), event.getId()); + result.put(FieldNames.RACE_LOG_EVENT_PASS_ID.name(), event.getPassId()); + result.put(FieldNames.RACE_LOG_EVENT_INVOLVED_BOATS.name(), storeInvolvedBoatsForRaceLogEvent(event.getInvolvedBoats())); + storeRaceLogEventAuthor(result, event.getAuthor()); + } + + + private BasicDBList storeInvolvedBoatsForRaceLogEvent(List competitors) { + BasicDBList dbInvolvedCompetitorIds = new BasicDBList(); + for (Competitor competitor : competitors) { + dbInvolvedCompetitorIds.add(competitor.getId()); + } + return dbInvolvedCompetitorIds; + } + + private DBObject storeRaceLogPassChangeEvent(RaceLogPassChangeEvent passChangeEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(passChangeEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogPassChangeEvent.class.getSimpleName()); + return result; + } + + private DBObject storeRaceLogDependentStartTimeEvent(RaceLogDependentStartTimeEvent dependentStartTimeEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(dependentStartTimeEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogDependentStartTimeEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_DEPDENDENT_ON_REGATTALIKE.name(), dependentStartTimeEvent.getDependentOnRaceIdentifier().getRegattaLikeParentName()); + result.put(FieldNames.RACE_LOG_DEPDENDENT_ON_RACECOLUMN.name(), dependentStartTimeEvent.getDependentOnRaceIdentifier().getRaceColumnName()); + result.put(FieldNames.RACE_LOG_DEPDENDENT_ON_FLEET.name(), dependentStartTimeEvent.getDependentOnRaceIdentifier().getFleetName()); + storeDuration(dependentStartTimeEvent.getStartTimeDifference(), result, FieldNames.RACE_LOG_START_TIME_DIFFERENCE_IN_MS); + result.put(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name(), dependentStartTimeEvent.getNextStatus().name()); + return result; + } + + private void storeDuration(Duration duration, DBObject result, FieldNames fieldName) { + if (duration != null) { + result.put(fieldName.name(), duration.asMillis()); + } + } + + private DBObject storeRaceLogRaceStatusEvent(RaceLogRaceStatusEvent raceStatusEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(raceStatusEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogRaceStatusEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_EVENT_NEXT_STATUS.name(), raceStatusEvent.getNextStatus().name()); + return result; + } + + private DBObject storeRaceLogCourseAreaChangedEvent(RaceLogCourseAreaChangedEvent courseAreaChangedEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(courseAreaChangedEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogCourseAreaChangedEvent.class.getSimpleName()); + result.put(FieldNames.COURSE_AREA_ID.name(), courseAreaChangedEvent.getCourseAreaId()); + return result; + } + + private DBObject storeRaceLogCourseDesignChangedEvent(RaceLogCourseDesignChangedEvent courseDesignChangedEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(courseDesignChangedEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogCourseDesignChangedEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_COURSE_DESIGN_NAME.name(), courseDesignChangedEvent.getCourseDesign().getName()); + result.put(FieldNames.RACE_LOG_COURSE_DESIGN.name(), storeCourseBase(courseDesignChangedEvent.getCourseDesign())); + return result; + } + + private Object storeRaceLogFinishPositioningListChangedEvent(RaceLogFinishPositioningListChangedEvent finishPositioningListChangedEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(finishPositioningListChangedEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFinishPositioningListChangedEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name(), storePositionedCompetitors(finishPositioningListChangedEvent.getPositionedCompetitorsIDsNamesMaxPointsReasons())); + + return result; + } + + private Object storeRaceLogFinishPositioningConfirmedEvent(RaceLogFinishPositioningConfirmedEvent finishPositioningConfirmedEvent) { + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(finishPositioningConfirmedEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogFinishPositioningConfirmedEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_POSITIONED_COMPETITORS.name(), storePositionedCompetitors(finishPositioningConfirmedEvent.getPositionedCompetitorsIDsNamesMaxPointsReasons())); + + return result; + } + + private Object storeRaceLogGateLineOpeningTimeEvent(RaceLogGateLineOpeningTimeEvent gateLineOpeningTimeEvent){ + DBObject result = new BasicDBObject(); + storeRaceLogEventProperties(gateLineOpeningTimeEvent, result); + result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogGateLineOpeningTimeEvent.class.getSimpleName()); + result.put(FieldNames.RACE_LOG_GATE_LINE_OPENING_TIME.name(), gateLineOpeningTimeEvent.getGateLineOpeningTimes().getGateLaunchStopTime()); + result.put(FieldNames.RACE_LOG_GOLF_DOWN_TIME.name(), gateLineOpeningTimeEvent.getGateLineOpeningTimes().getGolfDownTime()); + return result; + } + + private BasicDBList storePositionedCompetitors(List> positionedCompetitors) { + BasicDBList dbList = new BasicDBList(); + if (positionedCompetitors != null) { + for (com.sap.sse.common.Util.Triple competitorPair : positionedCompetitors) { + dbList.add(storePositionedCompetitor(competitorPair)); + } + } + return dbList; + } + + private DBObject storePositionedCompetitor(com.sap.sse.common.Util.Triple competitorTriple) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.COMPETITOR_ID.name(), competitorTriple.getA()); + result.put(FieldNames.COMPETITOR_DISPLAY_NAME.name(), competitorTriple.getB()); + result.put(FieldNames.LEADERBOARD_SCORE_CORRECTION_MAX_POINTS_REASON.name(), competitorTriple.getC().name()); + + return result; + } + + private BasicDBList storeCourseBase(CourseBase courseData) { + BasicDBList dbList = new BasicDBList(); + + for (Waypoint waypoint : courseData.getWaypoints()) { + dbList.add(storeWaypoint(waypoint)); + } + return dbList; + } + + private DBObject storeWaypoint(Waypoint waypoint) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.WAYPOINT_PASSINGINSTRUCTIONS.name(), getPassingInstructions(waypoint.getPassingInstructions())); + result.put(FieldNames.CONTROLPOINT.name(), storeControlPoint(waypoint.getControlPoint())); + return result; + } + + private DBObject storeControlPoint(ControlPoint controlPoint) { + DBObject result = new BasicDBObject(); + if (controlPoint instanceof Mark) { + result.put(FieldNames.CONTROLPOINT_CLASS.name(), Mark.class.getSimpleName()); + result.put(FieldNames.CONTROLPOINT_VALUE.name(), storeMark((Mark) controlPoint)); + } else if (controlPoint instanceof ControlPointWithTwoMarks) { + result.put(FieldNames.CONTROLPOINT_CLASS.name(), ControlPointWithTwoMarks.class.getSimpleName()); + result.put(FieldNames.CONTROLPOINT_VALUE.name(), storeControlPointWithTwoMarks((ControlPointWithTwoMarks) controlPoint)); + } + return result; + } + + private DBObject storeControlPointWithTwoMarks(ControlPointWithTwoMarks cpwtm) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_ID.name(), cpwtm.getId()); + result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_NAME.name(), cpwtm.getName()); + result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_LEFT.name(), storeMark(cpwtm.getLeft())); + result.put(FieldNames.CONTROLPOINTWITHTWOMARKS_RIGHT.name(), storeMark(cpwtm.getRight())); + return result; + } + + private DBObject storeMark(Mark mark) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.MARK_ID.name(), mark.getId()); + result.put(FieldNames.MARK_COLOR.name(), mark.getColor()); + result.put(FieldNames.MARK_NAME.name(), mark.getName()); + result.put(FieldNames.MARK_PATTERN.name(), mark.getPattern()); + result.put(FieldNames.MARK_SHAPE.name(), mark.getShape()); + result.put(FieldNames.MARK_TYPE.name(), mark.getType() == null ? null : mark.getType().name()); + return result; + } + + private String getPassingInstructions(PassingInstruction passingInstructions) { + final String passing; + if (passingInstructions != null) { + passing = passingInstructions.name(); + } else { + passing = null; + } + return passing; + } + @Override + public void storeCompetitor(Competitor competitor) { + DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); + JSONObject json = competitorSerializer.serialize(competitor); + DBObject query = (DBObject) JSON.parse(CompetitorJsonSerializer.getCompetitorIdQuery(competitor).toString()); + DBObject entry = (DBObject) JSON.parse(json.toString()); + collection.update(query, entry, /* upsrt */true, /* multi */false, WriteConcern.SAFE); + } + + @Override + public void removeAllCompetitors() { + logger.info("Removing all persistent competitor info"); + DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); + collection.drop(); + } + + @Override + public void removeCompetitor(Competitor competitor) { + logger.info("Removing persistent competitor info for competitor "+competitor.getName()+" with ID "+competitor.getId()); + DBCollection collection = database.getCollection(CollectionNames.COMPETITORS.name()); + DBObject query = (DBObject) JSON.parse(CompetitorJsonSerializer.getCompetitorIdQuery(competitor).toString()); + collection.remove(query, WriteConcern.SAFE); + } + + @Override + public void storeDeviceConfiguration(DeviceConfigurationMatcher matcher, DeviceConfiguration configuration) { + DBCollection configurationsCollections = database.getCollection(CollectionNames.CONFIGURATIONS.name()); + + DBObject query = new BasicDBObject(); + query.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); + + DBObject entryObject = new BasicDBObject(); + entryObject.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); + entryObject.put(FieldNames.CONFIGURATION_MATCHER.name(), createDeviceConfigurationMatcherObject(matcher)); + entryObject.put(FieldNames.CONFIGURATION_CONFIG.name(), createDeviceConfigurationObject(configuration)); + + configurationsCollections.update(query, entryObject, /* upsrt */ true, /* multi */ false, WriteConcern.SAFE); + } + + private DBObject createDeviceConfigurationMatcherObject(DeviceConfigurationMatcher matcher) { + DBObject matcherObject = new BasicDBObject(); + matcherObject.put(FieldNames.CONFIGURATION_MATCHER_TYPE.name(), matcher.getMatcherType().name()); + if (matcher instanceof DeviceConfigurationMatcherSingle) { + BasicDBList client = new BasicDBList(); + client.add(((DeviceConfigurationMatcherSingle)matcher).getClientIdentifier()); + matcherObject.put(FieldNames.CONFIGURATION_MATCHER_CLIENTS.name(), client); + } else if (matcher instanceof DeviceConfigurationMatcherMulti) { + BasicDBList clients = new BasicDBList(); + Util.addAll(((DeviceConfigurationMatcherMulti)matcher).getClientIdentifiers(), clients); + matcherObject.put(FieldNames.CONFIGURATION_MATCHER_CLIENTS.name(), clients); + } + return matcherObject; + } + + private DBObject createDeviceConfigurationObject(DeviceConfiguration configuration) { + JsonSerializer serializer = DeviceConfigurationJsonSerializer.create(); + JSONObject json = serializer.serialize(configuration); + DBObject entry = (DBObject) JSON.parse(json.toString()); + return entry; + } + + @Override + public void removeDeviceConfiguration(DeviceConfigurationMatcher matcher) { + DBCollection configurationsCollections = database.getCollection(CollectionNames.CONFIGURATIONS.name()); + DBObject query = new BasicDBObject(); + query.put(FieldNames.CONFIGURATION_MATCHER_ID.name(), matcher.getMatcherIdentifier()); + configurationsCollections.remove(query); + } + + public static DBObject storeDeviceId( + TypeBasedServiceFinder deviceIdentifierServiceFinder, DeviceIdentifier device) + throws TransformationException, NoCorrespondingServiceRegisteredException { + String type = device.getIdentifierType(); + DeviceIdentifierMongoHandler handler = deviceIdentifierServiceFinder.findService(type); + com.sap.sse.common.Util.Pair pair = handler.serialize(device); + type = pair.getA(); + Object deviceTypeSpecificId = pair.getB(); + return new BasicDBObjectBuilder() + .add(FieldNames.DEVICE_TYPE.name(), type) + .add(FieldNames.DEVICE_TYPE_SPECIFIC_ID.name(), deviceTypeSpecificId) + .add(FieldNames.DEVICE_STRING_REPRESENTATION.name(), device.getStringRepresentation()).get(); + } + + void storeRaceLogEventEvent(DBObject eventEntry) { + getRaceLogCollection().insert(eventEntry); + } + + @Override + public void removeRaceLog(RaceLogIdentifier identifier) { + DBObject query = new BasicDBObject(); + storeRaceLogIdentifier(identifier, query); + getRaceLogCollection().remove(query); + } + + @Override + public void removeRegattaLog(RegattaLikeIdentifier identifier) { + DBObject query = new BasicDBObject(); + addRegattaLikeIdentifier(identifier, query); + getRegattaLogCollection().remove(query); + } + + @Override + public void storeResultUrl(String resultProviderName, URL url) { + DBCollection resultUrlsCollection = database.getCollection(CollectionNames.RESULT_URLS.name()); + DBObject query = new BasicDBObject(FieldNames.RESULT_PROVIDERNAME.name(), resultProviderName); + DBObject entry = new BasicDBObject(FieldNames.RESULT_PROVIDERNAME.name(), resultProviderName); + entry.put(FieldNames.RESULT_URL.name(), url.toString()); + resultUrlsCollection.update(query, entry, /* upsrt */true, /* multi */false, WriteConcern.SAFE); + } + + @Override + public void removeResultUrl(String resultProviderName, URL url) { + DBCollection resultUrlsCollection = database.getCollection(CollectionNames.RESULT_URLS.name()); + DBObject query = new BasicDBObjectBuilder().add(FieldNames.RESULT_PROVIDERNAME.name(), resultProviderName) + .add(FieldNames.RESULT_URL.name(), url.toString()).get(); + resultUrlsCollection.remove(query); + } + + public DBCollection getRegattaLogCollection() { + DBCollection result = database.getCollection(CollectionNames.REGATTA_LOGS.name()); + DBObject index = new BasicDBObject(FieldNames.REGATTA_LOG_IDENTIFIER_TYPE.name(), null); + index.put(FieldNames.REGATTA_LOG_IDENTIFIER_NAME.name(), null); + result.createIndex(index); + return result; + } + + private DBObject createBasicRegattaLogEventDBObject(RegattaLogEvent event) { + DBObject result = new BasicDBObject(); + storeTimed(event, result); + storeTimePoint(event.getCreatedAt(), result, FieldNames.REGATTA_LOG_EVENT_CREATED_AT); + result.put(FieldNames.REGATTA_LOG_EVENT_ID.name(), event.getId()); + result.put(FieldNames.REGATTA_LOG_EVENT_AUTHOR_NAME.name(), event.getAuthor().getName()); + result.put(FieldNames.REGATTA_LOG_EVENT_AUTHOR_PRIORITY.name(), event.getAuthor().getPriority()); + return result; + } + + private void addRegattaLikeIdentifier(RegattaLikeIdentifier regattaLikeId, DBObject toObject) { + toObject.put(FieldNames.REGATTA_LOG_IDENTIFIER_TYPE.name(), regattaLikeId.getIdentifierType()); + toObject.put(FieldNames.REGATTA_LOG_IDENTIFIER_NAME.name(), regattaLikeId.getName()); + } + + private void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, DBObject innerObject) { + DBObject result = new BasicDBObject(FieldNames.REGATTA_LOG_EVENT.name(), innerObject); + addRegattaLikeIdentifier(regattaLikeId, result); + getRegattaLogCollection().insert(result); + } + + public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogDeviceCompetitorMappingEvent event) { + DBObject result = createBasicRegattaLogEventDBObject(event); + result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogDeviceCompetitorMappingEvent.class.getSimpleName()); + storeDeviceMappingEvent(event, result, FieldNames.REGATTA_LOG_FROM, FieldNames.REGATTA_LOG_TO); + result.put(FieldNames.COMPETITOR_ID.name(), event.getMappedTo().getId()); + storeRegattaLogEvent(regattaLikeId, result); + } + + public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogDeviceMarkMappingEvent event) { + DBObject result = createBasicRegattaLogEventDBObject(event); + result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogDeviceMarkMappingEvent.class.getSimpleName()); + storeDeviceMappingEvent(event, result, FieldNames.REGATTA_LOG_FROM, FieldNames.REGATTA_LOG_TO); + result.put(FieldNames.MARK.name(), storeMark(event.getMappedTo())); + storeRegattaLogEvent(regattaLikeId, result); + } + + public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogRevokeEvent event) { + DBObject result = createBasicRegattaLogEventDBObject(event); + result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogRevokeEvent.class.getSimpleName()); + result.put(FieldNames.REGATTA_LOG_REVOKED_EVENT_ID.name(), event.getRevokedEventId()); + result.put(FieldNames.REGATTA_LOG_REVOKED_EVENT_TYPE.name(), event.getRevokedEventType()); + result.put(FieldNames.REGATTA_LOG_REVOKED_EVENT_SHORT_INFO.name(), event.getRevokedEventShortInfo()); + result.put(FieldNames.REGATTA_LOG_REVOKED_REASON.name(), event.getReason()); + storeRegattaLogEvent(regattaLikeId, result); + } + + public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogRegisterCompetitorEvent event) { + DBObject result = createBasicRegattaLogEventDBObject(event); + result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogRegisterCompetitorEvent.class.getSimpleName()); + result.put(FieldNames.REGATTA_LOG_COMPETITOR_ID.name(), event.getCompetitor().getId()); + storeRegattaLogEvent(regattaLikeId, result); + } + + public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogCloseOpenEndedDeviceMappingEvent event) { + DBObject result = createBasicRegattaLogEventDBObject(event); + result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogCloseOpenEndedDeviceMappingEvent.class.getSimpleName()); + result.put(FieldNames.REGATTA_LOG_DEVICE_MAPPING_EVENT_ID.name(), event.getDeviceMappingEventId()); + storeTimePoint(event.getClosingTimePoint(), result, FieldNames.REGATTA_LOG_CLOSING_TIMEPOINT); + storeRegattaLogEvent(regattaLikeId, result); + } + + public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogSetCompetitorTimeOnTimeFactorEvent event) { + DBObject result = createBasicRegattaLogEventDBObject(event); + result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogCloseOpenEndedDeviceMappingEvent.class.getSimpleName()); + result.put(FieldNames.REGATTA_LOG_COMPETITOR_ID.name(), event.getCompetitor().getId()); + result.put(FieldNames.REGATTA_LOG_TIME_ON_TIME_FACTOR.name(), event.getTimeOnTimeFactor()); + } + + public DBObject storeRaceLogEntry(RaceLogIdentifier raceLogIdentifier, RaceLogDependentStartTimeEvent event) { + BasicDBObject result = new BasicDBObject(); + storeRaceLogIdentifier(raceLogIdentifier, result); + result.put(FieldNames.RACE_LOG_EVENT.name(), storeRaceLogDependentStartTimeEvent(event)); + return result; + } + + public void storeRegattaLogEvent(RegattaLikeIdentifier regattaLikeId, RegattaLogSetCompetitorTimeOnDistanceAllowancePerNauticalMileEvent event) { + DBObject result = createBasicRegattaLogEventDBObject(event); + result.put(FieldNames.REGATTA_LOG_EVENT_CLASS.name(), RegattaLogCloseOpenEndedDeviceMappingEvent.class.getSimpleName()); + result.put(FieldNames.REGATTA_LOG_COMPETITOR_ID.name(), event.getCompetitor().getId()); + result.put(FieldNames.REGATTA_LOG_TIME_ON_DISTANCE_SECONDS_ALLOWANCE_PER_NAUTICAL_MILE.name(), event.getTimeOnDistanceAllowancePerNauticalMile().asSeconds()); + storeRegattaLogEvent(regattaLikeId, result); + } + + private DBObject createImageObject(ImageDescriptor image) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.IMAGE_URL.name(), image.getURL().toString()); + result.put(FieldNames.IMAGE_LOCALE.name(), image.getLocale() != null ? image.getLocale().toLanguageTag() : null); + result.put(FieldNames.IMAGE_TITLE.name(), image.getTitle()); + result.put(FieldNames.IMAGE_SUBTITLE.name(), image.getSubtitle()); + result.put(FieldNames.IMAGE_COPYRIGHT.name(), image.getCopyright()); + result.put(FieldNames.IMAGE_WIDTH_IN_PX.name(), image.getWidthInPx()); + result.put(FieldNames.IMAGE_HEIGHT_IN_PX.name(), image.getHeightInPx()); + storeTimePoint(image.getCreatedAtDate(), result, FieldNames.IMAGE_CREATEDATDATE); + BasicDBList tags = new BasicDBList(); + for (String tag : image.getTags()) { + tags.add(tag); + } + result.put(FieldNames.IMAGE_TAGS.name(), tags); + return result; + } + + private DBObject createVideoObject(VideoDescriptor video) { + DBObject result = new BasicDBObject(); + result.put(FieldNames.VIDEO_URL.name(), video.getURL().toString()); + result.put(FieldNames.VIDEO_LOCALE.name(), video.getLocale() != null ? video.getLocale().toLanguageTag() : null); + result.put(FieldNames.VIDEO_THUMBNAIL_URL.name(), video.getThumbnailURL() != null ? video.getThumbnailURL().toString() : null); + result.put(FieldNames.VIDEO_TITLE.name(), video.getTitle()); + result.put(FieldNames.VIDEO_SUBTITLE.name(), video.getSubtitle()); + result.put(FieldNames.VIDEO_MIMETYPE.name(), video.getMimeType() != null ? video.getMimeType().name() : null); + result.put(FieldNames.VIDEO_COPYRIGHT.name(), video.getCopyright()); + result.put(FieldNames.VIDEO_LENGTH_IN_SECONDS.name(), video.getLengthInSeconds()); + storeTimePoint(video.getCreatedAtDate(), result, FieldNames.VIDEO_CREATEDATDATE); + BasicDBList tags = new BasicDBList(); + for (String tag : video.getTags()) { + tags.add(tag); + } + result.put(FieldNames.VIDEO_TAGS.name(), tags); + return result; + } +} diff --git a/java/com.sap.sailing.domain.shared.android/pom.xml b/java/com.sap.sailing.domain.shared.android/pom.xml index b6b4b65e730..af7117f708a 100644 --- a/java/com.sap.sailing.domain.shared.android/pom.xml +++ b/java/com.sap.sailing.domain.shared.android/pom.xml @@ -1,39 +1,39 @@ - - - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - com.sap.sailing.domain.shared.android - eclipse-plugin - - - - org.eclipse.tycho - tycho-compiler-plugin - ${tycho-version} - - 1.7 - 1.7 - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - - - + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.domain.shared.android + eclipse-plugin + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + 1.7 + 1.7 + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/EventBase.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/EventBase.java index f642b939199..1a6a72c7650 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/EventBase.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/EventBase.java @@ -1,98 +1,98 @@ -package com.sap.sailing.domain.base; - -import java.net.URL; -import java.util.Map; - -import com.sap.sailing.domain.common.Renamable; -import com.sap.sse.common.Named; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.WithID; -import com.sap.sse.common.media.ImageSize; -import com.sap.sse.common.media.WithMedia; - -/** - * Base interface for an Event consisting of all static information, which might be shared - * by the server and an Android application. - */ -public interface EventBase extends Named, WithDescription, Renamable, WithID, WithMedia { - - void setDescription(String description); - - /** - * @return a non-null venue for this event - */ - Venue getVenue(); - - /** - * @return the start date of the event - */ - TimePoint getStartDate(); - - void setStartDate(TimePoint startDate); - - /** - * @return the end date of the event - */ - TimePoint getEndDate(); - - void setEndDate(TimePoint startDate); - - boolean isPublic(); - - void setPublic(boolean isPublic); - - /** - * @deprecated - * Returns a non-null live but unmodifiable collection of URLs pointing to image resources that can be - * used to represent the event, e.g., on a web page. - * - * @return a non-null value which may be empty - */ - Iterable getImageURLs(); - - /** - * @deprecated - * An event may have zero or more sponsors, each of which usually want to see their logo on the web page. - * - * @return the sponsors' logos; always non-null but possibly empty - */ - Iterable getSponsorImageURLs(); - - /** - * @deprecated - * An optional logo image; may return null. - */ - URL getLogoImageURL(); - - /** - * @deprecated - * Returns a non-null live but unmodifiable collection of URLs pointing to video resources that can be - * used to represent the event, e.g., on a web page. - * - * @return a non-null value which may be empty - */ - Iterable getVideoURLs(); - - /** - * @return the URL of the event's official web site, or null if such a site does not exist or its URL - * is not known - */ - URL getOfficialWebsiteURL(); - - void setOfficialWebsiteURL(URL officialWebsiteURL); - - /** - * @return the URL of an external web site containing sailor related information like protests, official results, etc. - * or null if such a site does not exist or its URL is not known. - */ - URL getSailorsInfoWebsiteURL(); - - void setSailorsInfoWebsiteURL(URL sailorsInfoWebsiteURL); - - Iterable getLeaderboardGroups(); - - /** - * Sets and converts all event images and videos from the old URL based format to the new richer format - * */ - boolean setMediaURLs(Iterable imageURLs, Iterable sponsorImageURLs, Iterable videoURLs, URL logoImageURL, Map imageSizes); -} +package com.sap.sailing.domain.base; + +import java.net.URL; +import java.util.Map; + +import com.sap.sailing.domain.common.Renamable; +import com.sap.sse.common.Named; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.WithID; +import com.sap.sse.common.media.ImageSize; +import com.sap.sse.shared.media.WithMedia; + +/** + * Base interface for an Event consisting of all static information, which might be shared + * by the server and an Android application. + */ +public interface EventBase extends Named, WithDescription, Renamable, WithID, WithMedia { + + void setDescription(String description); + + /** + * @return a non-null venue for this event + */ + Venue getVenue(); + + /** + * @return the start date of the event + */ + TimePoint getStartDate(); + + void setStartDate(TimePoint startDate); + + /** + * @return the end date of the event + */ + TimePoint getEndDate(); + + void setEndDate(TimePoint startDate); + + boolean isPublic(); + + void setPublic(boolean isPublic); + + /** + * @deprecated + * Returns a non-null live but unmodifiable collection of URLs pointing to image resources that can be + * used to represent the event, e.g., on a web page. + * + * @return a non-null value which may be empty + */ + Iterable getImageURLs(); + + /** + * @deprecated + * An event may have zero or more sponsors, each of which usually want to see their logo on the web page. + * + * @return the sponsors' logos; always non-null but possibly empty + */ + Iterable getSponsorImageURLs(); + + /** + * @deprecated + * An optional logo image; may return null. + */ + URL getLogoImageURL(); + + /** + * @deprecated + * Returns a non-null live but unmodifiable collection of URLs pointing to video resources that can be + * used to represent the event, e.g., on a web page. + * + * @return a non-null value which may be empty + */ + Iterable getVideoURLs(); + + /** + * @return the URL of the event's official web site, or null if such a site does not exist or its URL + * is not known + */ + URL getOfficialWebsiteURL(); + + void setOfficialWebsiteURL(URL officialWebsiteURL); + + /** + * @return the URL of an external web site containing sailor related information like protests, official results, etc. + * or null if such a site does not exist or its URL is not known. + */ + URL getSailorsInfoWebsiteURL(); + + void setSailorsInfoWebsiteURL(URL sailorsInfoWebsiteURL); + + Iterable getLeaderboardGroups(); + + /** + * Sets and converts all event images and videos from the old URL based format to the new richer format + * */ + boolean setMediaURLs(Iterable imageURLs, Iterable sponsorImageURLs, Iterable videoURLs, URL logoImageURL, Map imageSizes); +} diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/EventBaseImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/EventBaseImpl.java index 566fce6e2c9..98ddd1f7e3f 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/EventBaseImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/base/impl/EventBaseImpl.java @@ -15,15 +15,15 @@ import com.sap.sailing.domain.base.Venue; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; -import com.sap.sse.common.media.ImageDescriptor; -import com.sap.sse.common.media.ImageDescriptorImpl; import com.sap.sse.common.media.ImageSize; -import com.sap.sse.common.media.MediaDescriptor; import com.sap.sse.common.media.MediaTagConstants; -import com.sap.sse.common.media.MediaUtils; import com.sap.sse.common.media.MimeType; -import com.sap.sse.common.media.VideoDescriptor; -import com.sap.sse.common.media.VideoDescriptorImpl; +import com.sap.sse.shared.media.ImageDescriptor; +import com.sap.sse.shared.media.MediaDescriptor; +import com.sap.sse.shared.media.MediaUtils; +import com.sap.sse.shared.media.VideoDescriptor; +import com.sap.sse.shared.media.impl.ImageDescriptorImpl; +import com.sap.sse.shared.media.impl.VideoDescriptorImpl; public abstract class EventBaseImpl implements EventBase { private static final long serialVersionUID = -5749964088848611074L; diff --git a/java/com.sap.sailing.domain.test/resources/IncrementalLeaderboardDTO.ser b/java/com.sap.sailing.domain.test/resources/IncrementalLeaderboardDTO.ser index 1fd2fb452b3852aa12c930a50dd7da94142d587e..8b0225928aa4a94d0309a1e921083f65d7f937af 100755 GIT binary patch literal 271138 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

    1%R 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

    >UIwiURVPWTJ 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 literal 223080 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

    @oN;}muveC?4$!-dorBZFPLRxKI;=2KQY~3 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 z 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 YX*Fx5ruC>rwpA;JD*%QoHX{B10ccIKsQ>@~ diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java index 7e7a7e29630..4b91a74d1aa 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/MultipleClassesInRegattaTest.java @@ -60,7 +60,7 @@ public class MultipleClassesInRegattaTest { httpAndHost + "/events/event_20110505_SailingTea/clientparams.php?event=event_20110505_SailingTea&race=cce678c8-97e6-11e0-9aed-406186cbf87c"), new URI(liveURI), new URI(storedURI), new URI(courseDesignUpdateURI), - /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* simulateWithStartTimeNow */ false, /* ignoreTracTracMarkPassings*/ + /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, @@ -71,7 +71,7 @@ public class MultipleClassesInRegattaTest { httpAndHost + "/events/event_20110505_SailingTea/clientparams.php?event=event_20110505_SailingTea&race=11290bd6-97e7-11e0-9aed-406186cbf87c"), new URI(liveURI), new URI(storedURI), new URI(courseDesignUpdateURI), - /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* simulateWithStartTimeNow */ false, /* ignoreTracTracMarkPassings*/ + /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, @@ -82,7 +82,7 @@ public class MultipleClassesInRegattaTest { httpAndHost + "/events/event_20110505_SailingTea/clientparams.php?event=event_20110505_SailingTea&race=39635b24-97e7-11e0-9aed-406186cbf87c"), new URI(liveURI), new URI(storedURI), new URI(courseDesignUpdateURI), - /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* simulateWithStartTimeNow */ false, /* ignoreTracTracMarkPassings*/ + /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, @@ -93,7 +93,7 @@ public class MultipleClassesInRegattaTest { httpAndHost + "/events/event_20110505_SailingTea/clientparams.php?event=event_20110505_SailingTea&race=04498426-7dfd-11e0-8236-406186cbf87c"), new URI(liveURI), new URI(storedURI), new URI(courseDesignUpdateURI), - /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* simulateWithStartTimeNow */ false, /* ignoreTracTracMarkPassings*/ + /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, tracTracUsername, tracTracPassword, TracTracConnectionConstants.ONLINE_STATUS, TracTracConnectionConstants.ONLINE_VISIBILITY, diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java index eefd9495d9c..3b907f3cf30 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/UnicodeCharactersInCompetitorNamesTest.java @@ -67,7 +67,7 @@ public class UnicodeCharactersInCompetitorNamesTest { + TracTracConnectionConstants.PORT_STORED), new URI( "http://tracms.traclive.dk/update_course"), /* startOfTracking */null, /* endOfTracking */null, /* delayToLiveInMillis */0l, - /* simulateWithStartTimeNow */ false, /* ignoreTracTracMarkPassings*/ + /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings*/ false, EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, EmptyWindStore.INSTANCE, EmptyGPSFixStore.INSTANCE, "tracTest", "tracTest", "", "", new DummyTrackedRegattaRegistry(), mock(RaceLogResolver.class)); diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java index 2ee226e8b63..eca3d7cebdc 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java @@ -44,6 +44,7 @@ import com.sap.sailing.domain.tracking.impl.EmptyWindStore; import com.sap.sailing.domain.tractracadapter.impl.DomainFactoryImpl; import com.sap.sailing.domain.tractracadapter.impl.RaceCourseReceiver; import com.sap.sailing.domain.tractracadapter.impl.Simulator; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.tractrac.model.lib.api.data.IPosition; @@ -95,7 +96,6 @@ public interface DomainFactory { * Fetch a race definition previously created by a call to {@link #getOrCreateRaceDefinitionAndTrackedRace}. If no such * race definition was created so far, the call blocks until such a definition is provided by a call to * {@link #getOrCreateRaceDefinitionAndTrackedRace}. - * @param raceId TODO */ RaceDefinition getAndWaitForRaceDefinition(UUID raceId); @@ -105,7 +105,6 @@ public interface DomainFactory { * A new {@link com.sap.sailing.domain.base.Regatta} is created if no event by * an equal name with a boat class with an equal name as the event's * boat class exists yet. - * @param trackedRegattaRegistry TODO */ com.sap.sailing.domain.base.Regatta getOrCreateDefaultRegatta(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, IRace race, TrackedRegattaRegistry trackedRegattaRegistry); @@ -134,27 +133,30 @@ public interface DomainFactory { * if null, all stored data until the "end of time" will be loaded that the event has * to provide, particularly for the mark positions which are stored per event, not per race; otherwise, * particularly the mark position loading will be constrained to this end time. + * @param offsetToStartTimeOfSimulatedRace + * if non-null, the {@link Simulator} will be used with this duration as start offset * @param windStore * Provides the capability to obtain the {@link WindTrack}s for the different wind sources. A trivial * implementation is {@link EmptyWindStore} which simply provides new, empty tracks. This is always * available but loses track of the wind, e.g., during server restarts. - * @param raceLogResolver TODO */ TracTracRaceTracker createRaceTracker(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException; /** - * Same as {@link #createRaceTracker(URL, URI, URI, URI, TimePoint, TimePoint, WindStore, TrackedRegattaRegistry)}, only that - * a predefined {@link Regatta} is used to hold the resulting races. - * @param raceLogResolver TODO + * Same as {@link #createRaceTracker(URL, URI, URI, URI, TimePoint, TimePoint, WindStore, TrackedRegattaRegistry)}, + * only that a predefined {@link Regatta} is used to hold the resulting races. + * + * @param offsetToStartTimeOfSimulatedRace + * if non-null, the {@link Simulator} will be used with this duration as start offset */ RaceTracker createRaceTracker(Regatta regatta, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, @@ -262,9 +264,13 @@ public interface DomainFactory { Util.Pair, BoatClass> getCompetitorsAndDominantBoatClass(IRace race); + /** + * @param offsetToStartTimeOfSimulatedRace + * if non-null, the {@link Simulator} will be used with this duration as start offset + */ RaceTrackingConnectivityParameters createTrackingConnectivityParameters(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility); /** * Removes all knowledge about tractracRace which includes removing it from the race cache, from the diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java index 72fcb93662e..ca159e2ca8e 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/TracTracAdapter.java @@ -17,6 +17,8 @@ import com.sap.sailing.domain.regattalog.RegattaLogStore; import com.sap.sailing.domain.tracking.RaceHandle; import com.sap.sailing.domain.tracking.TrackerManager; import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.domain.tractracadapter.impl.Simulator; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -65,12 +67,13 @@ public interface TracTracAdapter { * created, with a single default series and a single default fleet. If a valid {@link RegattaIdentifier} * is specified, a regatta lookup is performed with that identifier; if the regatta is found, it is used * to add the races to. Otherwise, a default regatta as described above will be created and used. - * @param raceStatus + * @param offsetToStartTimeOfSimulatedRace + * if non-null, the {@link Simulator} will be used with this duration as start offset */ RaceHandle addTracTracRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint trackingStartTime, TimePoint trackingEndTime, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, - long timeoutForReceivingRaceDefinitionInMilliseconds, boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, + long timeoutForReceivingRaceDefinitionInMilliseconds, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility) throws MalformedURLException, FileNotFoundException, URISyntaxException, Exception; diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java index ec6c5959d06..7df01e515ff 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java @@ -74,6 +74,7 @@ import com.sap.sailing.domain.tractracadapter.ReceiverType; import com.sap.sailing.domain.tractracadapter.TracTracConfiguration; import com.sap.sailing.domain.tractracadapter.TracTracControlPoint; import com.sap.sailing.domain.tractracadapter.TracTracRaceTracker; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; @@ -652,27 +653,27 @@ public class DomainFactoryImpl implements DomainFactory { @Override public TracTracRaceTracker createRaceTracker(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException { return new TracTracRaceTrackerImpl(this, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, - endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, + endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); } @Override public RaceTracker createRaceTracker(Regatta regatta, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException { return new TracTracRaceTrackerImpl(regatta, this, paramURL, liveURI, storedURI, courseDesignUpdateURI, - startOfTracking, endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, + startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); } @@ -690,11 +691,11 @@ public class DomainFactoryImpl implements DomainFactory { @Override public RaceTrackingConnectivityParameters createTrackingConnectivityParameters(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, - long delayToLiveInMillis, boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, + long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility) { return new RaceTrackingConnectivityParametersImpl(paramURL, liveURI, storedURI, courseDesignUpdateURI, - startOfTracking, endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, + startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, this, tracTracUsername, tracTracPassword, raceStatus, raceVisibility); } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java index a438f5b0450..17c00105762 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceTrackingConnectivityParametersImpl.java @@ -16,6 +16,7 @@ import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.domain.tracking.WindStore; import com.sap.sailing.domain.tractracadapter.DomainFactory; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.tractrac.model.lib.api.event.CreateModelException; import com.tractrac.subscription.lib.api.SubscriberInitializationException; @@ -31,7 +32,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne private final RegattaLogStore regattaLogStore; private final DomainFactory domainFactory; private final long delayToLiveInMillis; - private final boolean simulateWithStartTimeNow; + private final Duration offsetToStartTimeOfSimulatedRace; private final String tracTracUsername; private final String tracTracPassword; private final String raceStatus; @@ -40,7 +41,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne public RaceTrackingConnectivityParametersImpl(URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, DomainFactory domainFactory, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility) { super(); @@ -52,7 +53,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne this.endOfTracking = endOfTracking; this.delayToLiveInMillis = delayToLiveInMillis; this.domainFactory = domainFactory; - this.simulateWithStartTimeNow = simulateWithStartTimeNow; + this.offsetToStartTimeOfSimulatedRace = offsetToStartTimeOfSimulatedRace; this.raceLogStore = raceLogStore; this.regattaLogStore = regattaLogStore; this.tracTracUsername = tracTracUsername; @@ -67,7 +68,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne GPSFixStore gpsFixStore, RaceLogResolver raceLogResolver) throws MalformedURLException, FileNotFoundException, URISyntaxException, CreateModelException, SubscriberInitializationException { RaceTracker tracker = domainFactory.createRaceTracker(paramURL, liveURI, storedURI, courseDesignUpdateURI, - startOfTracking, endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, + startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); return tracker; @@ -77,7 +78,7 @@ public class RaceTrackingConnectivityParametersImpl implements RaceTrackingConne public RaceTracker createRaceTracker(Regatta regatta, TrackedRegattaRegistry trackedRegattaRegistry, WindStore windStore, GPSFixStore gpsFixStore, RaceLogResolver raceLogResolver) throws Exception { RaceTracker tracker = domainFactory.createRaceTracker(regatta, paramURL, liveURI, storedURI, - courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, + courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); return tracker; diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java index 2e5cfea7755..7e5049f9cf4 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RawPositionReceiver.java @@ -4,7 +4,6 @@ import java.util.logging.Logger; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.common.tracking.GPSFixMoving; -import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; import com.sap.sailing.domain.tracking.DynamicTrackedRace; import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; import com.sap.sailing.domain.tractracadapter.DomainFactory; @@ -57,13 +56,13 @@ public class RawPositionReceiver extends AbstractReceiverWithQueuetimed's time point according to this simulator's delay and waits roughly until this time @@ -157,12 +171,12 @@ public class Simulator { * Like {@link #delay}, only that it doesn't wait until timePoint is reached in wall time. */ public TimePoint advance(TimePoint timePoint) { - return new MillisecondsTimePoint(timePoint.asMillis()+getAdvanceInMillis()); + return timePoint.plus(getAdvance()); } /** * If {@link #advanceInMillis} is already set to a non-negative value, it is left alone, and - * {@link #delay(TimePoint)} is called. Otherwise, time is taken to be the original start time of the + * {@link #advance(TimePoint)} is called. Otherwise, time is taken to be the original start time of the * race which is then used to compute {@link #advanceInMillis} such that * time.asMillis() + advanceInMillis == System.currentTimeMillis(). */ @@ -183,13 +197,13 @@ public class Simulator { if (isAdvanceInMillisSet()) { return advance(time); } else { - setAdvanceInMillis(System.currentTimeMillis() - time.asMillis()); + setAdvanceInMillis(new MillisecondsDurationImpl(MillisecondsTimePoint.now().minus(time.asMillis()).plus(getOffsetToStart()).asMillis())); return advance(time); } } private boolean isAdvanceInMillisSet() { - return advanceInMillis != -1; + return !advanceInMillis.equals(Duration.NULL.minus(1)); } /** @@ -238,21 +252,35 @@ public class Simulator { // deliver an empty list now trackedRace.updateMarkPassings(competitor, markPassings); } - + } + + public void scheduleCompetitorPosition(final Competitor competitor, GPSFixMoving competitorFix) { + final RecordGPSFix recorder = (c, f)->trackedRace.recordFix(c, f); + scheduleFixRecording(competitor, competitorFix, recorder); + } + + @FunctionalInterface + private static interface RecordGPSFix { + void recordFix(T objectForFix, GPSFixMoving fix); } public void scheduleMarkPosition(final Mark mark, GPSFixMoving markFix) { - final TimePoint transformedTimepoint = advance(markFix.getTimePoint()); - final GPSFixMoving transformedMarkFix = new GPSFixMovingImpl(markFix.getPosition(), transformedTimepoint, markFix.getSpeed()); + final RecordGPSFix recorder = (m, f)->trackedRace.recordFix(mark, f); + scheduleFixRecording(mark, markFix, recorder); + } + + private void scheduleFixRecording(final T object, GPSFixMoving fix, final RecordGPSFix recorder) { + final TimePoint transformedTimepoint = advance(fix.getTimePoint()); + final GPSFixMoving transformedMarkFix = new GPSFixMovingImpl(fix.getPosition(), transformedTimepoint, fix.getSpeed()); long waitTime = getWaitTimeInMillisUntil(transformedMarkFix.getTimePoint()); if (waitTime <= 0) { - trackedRace.recordFix(mark, transformedMarkFix); + recorder.recordFix(object, transformedMarkFix); } else { timer.schedule(new TimerTask() { @Override public void run() { try { - trackedRace.recordFix(mark, transformedMarkFix); + recorder.recordFix(object, transformedMarkFix); } catch (Exception e) { logger.throwing(Simulator.class.getName(), "scheduleMarkPosition", e); } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java index 8264853e299..0d7fd26a6ec 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracAdapterImpl.java @@ -21,6 +21,7 @@ import com.sap.sailing.domain.tractracadapter.JSONService; import com.sap.sailing.domain.tractracadapter.RaceRecord; import com.sap.sailing.domain.tractracadapter.TracTracAdapter; import com.sap.sailing.domain.tractracadapter.TracTracConfiguration; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -56,7 +57,7 @@ public class TracTracAdapterImpl implements TracTracAdapter { getTracTracDomainFactory().createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI, /* startOfTracking */null, - /* endOfTracking */null, delayToLiveInMillis, /* simulateWithStartTimeNow */false, /* ignoreTracTracMarkPassings */ false, + /* endOfTracking */null, delayToLiveInMillis, /* offsetToStartTimeOfSimulatedRace */null, /* ignoreTracTracMarkPassings */ false, raceLogStore, regattaLogStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility), timeoutInMilliseconds); } @@ -65,13 +66,13 @@ public class TracTracAdapterImpl implements TracTracAdapter { public RaceHandle addTracTracRace(TrackerManager trackerManager, RegattaIdentifier regattaToAddTo, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, long timeoutInMilliseconds, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword, String raceStatus, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility) throws Exception { return trackerManager.addRace( regattaToAddTo, getTracTracDomainFactory().createTrackingConnectivityParameters(paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, - simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, tracTracUsername, tracTracPassword, + offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility), timeoutInMilliseconds); } diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java index 4dac78b6701..50c376d419c 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java @@ -49,6 +49,7 @@ import com.sap.sailing.domain.tractracadapter.DomainFactory; import com.sap.sailing.domain.tractracadapter.Receiver; import com.sap.sailing.domain.tractracadapter.TracTracConnectionConstants; import com.sap.sailing.domain.tractracadapter.TracTracRaceTracker; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.tractrac.model.lib.api.ModelLocator; @@ -253,26 +254,26 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements */ protected TracTracRaceTrackerImpl(DomainFactory domainFactory, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, long delayToLiveInMillis, - boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws URISyntaxException, MalformedURLException, FileNotFoundException, CreateModelException, SubscriberInitializationException { this(ModelLocator.getEventFactory().createRace(new URI(paramURL.toString())), domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, - simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, + offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); } private TracTracRaceTrackerImpl(IRace tractracRace, DomainFactory domainFactory, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, - long delayToLiveInMillis, boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, + long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws URISyntaxException, MalformedURLException, FileNotFoundException, SubscriberInitializationException { this(tractracRace, null, domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, - endOfTracking, delayToLiveInMillis, simulateWithStartTimeNow, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, + endOfTracking, delayToLiveInMillis, offsetToStartTimeOfSimulatedRace, useInternalMarkPassingAlgorithm, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); } @@ -284,14 +285,14 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements */ protected TracTracRaceTrackerImpl(Regatta regatta, DomainFactory domainFactory, URL paramURL, URI liveURI, URI storedURI, URI courseDesignUpdateURI, TimePoint startOfTracking, TimePoint endOfTracking, - long delayToLiveInMillis, boolean simulateWithStartTimeNow, boolean ignoreTracTracMarkPassings, RaceLogStore raceLogStore, + long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean ignoreTracTracMarkPassings, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws URISyntaxException, MalformedURLException, FileNotFoundException, CreateModelException, SubscriberInitializationException { this(ModelLocator.getEventFactory().createRace(new URI(paramURL.toString())), regatta, domainFactory, paramURL, liveURI, storedURI, courseDesignUpdateURI, startOfTracking, endOfTracking, delayToLiveInMillis, - simulateWithStartTimeNow, ignoreTracTracMarkPassings, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, + offsetToStartTimeOfSimulatedRace, ignoreTracTracMarkPassings, raceLogStore, regattaLogStore, windStore, gpsFixStore, tracTracUsername, tracTracPassword, raceStatus, raceVisibility, trackedRegattaRegistry, raceLogResolver); } @@ -300,15 +301,15 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements * @param regatta * if null, then domainFactory.getOrCreateRegatta(tractracEvent) will be used * to obtain a default regatta - * @param simulateWithStartTimeNow - * if true, the connector will adjust the time stamps of all events received such that the + * @param offsetToStartTimeOfSimulatedRace + * if not null, the connector will adjust the time stamps of all events received such that the * first mark passing for the first waypoint will be set to "now." It will delay the forwarding of all - * events received such that they seem to be sent in "real-time." So, more or less the time points + * events received such that they seem to be sent in "real-time" + offsetToStartTimeOfSimulatedRace So, more or less the time points * attached to the events sent to the receivers will again approximate the wall time. */ private TracTracRaceTrackerImpl(IRace tractracRace, final Regatta regatta, DomainFactory domainFactory, URL paramURL, URI liveURI, URI storedURI, URI tracTracUpdateURI, TimePoint startOfTracking, - TimePoint endOfTracking, long delayToLiveInMillis, boolean simulateWithStartTimeNow, boolean useInternalMarkPassingAlgorithm, + TimePoint endOfTracking, long delayToLiveInMillis, Duration offsetToStartTimeOfSimulatedRace, boolean useInternalMarkPassingAlgorithm, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, GPSFixStore gpsFixStore, String tracTracUsername, String tracTracPassword, String raceStatus, String raceVisibility, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver) throws URISyntaxException, MalformedURLException, @@ -323,8 +324,8 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl implements this.gpsFixStore = gpsFixStore; this.domainFactory = domainFactory; this.lastProgressPerID = new HashMap<>(); - if (simulateWithStartTimeNow) { - simulator = new Simulator(windStore); + if (offsetToStartTimeOfSimulatedRace != null) { + simulator = new Simulator(windStore, offsetToStartTimeOfSimulatedRace); // don't write the transformed wind fixes into the DB again... see also bug 1974 this.windStore = EmptyWindStore.INSTANCE; } else { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Course.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Course.java index fb1dadb2f61..b86693c71cf 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Course.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/Course.java @@ -43,4 +43,6 @@ public interface Course extends CourseBase { void update(Iterable> newControlPoints, DomainFactory baseDomainFactory) throws PatchFailedException; int getNumberOfWaypoints(); + + Leg getLeg(int zeroBasedIndexOfWaypoint); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/CourseImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/CourseImpl.java index b68ea6ac7ff..e3e39e010c1 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/CourseImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/CourseImpl.java @@ -246,6 +246,11 @@ public class CourseImpl extends NamedImpl implements Course { return waypoints.size(); } + @Override + public Leg getLeg(int zeroBasedIndexOfWaypoint) { + return legs.get(zeroBasedIndexOfWaypoint); + } + @Override public String toString() { lockForRead(); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java index de92f254836..454843ba3e2 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java @@ -1,1907 +1,1937 @@ -package com.sap.sailing.domain.leaderboard.impl; - -import java.io.IOException; -import java.io.ObjectInputStream; -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.NavigableSet; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.RunnableFuture; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.sap.sailing.domain.abstractlog.race.InvalidatesLeaderboardCache; -import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.Course; -import com.sap.sailing.domain.base.DomainFactory; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Leg; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnInSeries; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.LegType; -import com.sap.sailing.domain.common.ManeuverType; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.common.NoWindError; -import com.sap.sailing.domain.common.NoWindException; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.RegattaNameAndRaceName; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.SpeedWithBearing; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.dto.FleetDTO; -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.RaceDTO; -import com.sap.sailing.domain.common.tracking.GPSFixMoving; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.NumberOfCompetitorsInLeaderboardFetcher; -import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.ScoreCorrection.Result; -import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCache; -import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; -import com.sap.sailing.domain.leaderboard.caching.LiveLeaderboardUpdater; -import com.sap.sailing.domain.racelog.RaceLogIdentifier; -import com.sap.sailing.domain.ranking.RankingMetric.CompetitorRankingInfo; -import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; -import com.sap.sailing.domain.tracking.GPSFixTrack; -import com.sap.sailing.domain.tracking.Maneuver; -import com.sap.sailing.domain.tracking.MarkPassing; -import com.sap.sailing.domain.tracking.MarkPassingManeuver; -import com.sap.sailing.domain.tracking.RaceChangeListener; -import com.sap.sailing.domain.tracking.TrackedLeg; -import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; -import com.sap.sailing.domain.tracking.WindLegTypeAndLegBearingCache; -import com.sap.sailing.domain.tracking.WindPositionMode; -import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; -import com.sap.sailing.util.impl.RaceColumnListeners; -import com.sap.sse.common.Duration; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.Util; -import com.sap.sse.common.impl.MillisecondsTimePoint; -import com.sap.sse.concurrent.LockUtil; -import com.sap.sse.concurrent.NamedReentrantReadWriteLock; -import com.sap.sse.util.impl.ThreadFactoryWithPriority; - -/** - * Base implementation for various types of leaderboards. The {@link RaceColumnListener} implementation forwards events - * received to all {@link RaceColumnListener} subscribed with this leaderboard. To which objects this leaderboard subscribes - * as {@link RaceColumnListener} is left to the concrete subclasses to implement, but the race columns seem like useful - * candidates. - * - * @author Axel Uhl (d043530) - * - */ -public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, RaceColumnListener { - private static final long serialVersionUID = 330156778603279333L; - - private static final Logger logger = Logger.getLogger(AbstractSimpleLeaderboardImpl.class.getName()); - - static final Double DOUBLE_0 = new Double(0); - - private final SettableScoreCorrection scoreCorrection; - - private ThresholdBasedResultDiscardingRule crossLeaderboardResultDiscardingRule; - - /** - * The optional display name mappings for competitors. This allows a user to override the tracking-provided - * competitor names for display in a leaderboard. - */ - private final Map displayNames; - - /** the display name of the leaderboard */ - private String displayName; - - /** - * Backs the {@link #getCarriedPoints(Competitor)} API with data. Can be used to prime this leaderboard - * with aggregated results of races not tracked / displayed by this leaderboard in detail. The points - * provided by this map are considered by {@link #getTotalPoints(Competitor, TimePoint)}. - */ - private final Map carriedPoints; - - private final RaceColumnListeners raceColumnListeners; - - /** - * A set that manages the difference between {@link #getCompetitors()} and {@link #getAllCompetitors()}. Access - * is controlled by the {@link #suppressedCompetitorsLock} lock. - */ - private final Set suppressedCompetitors; - private final NamedReentrantReadWriteLock suppressedCompetitorsLock; - - private transient Map, RunnableFuture> raceDetailsAtEndOfTrackingCache; - - /** - * This executor needs to be a different one than {@link #executor} because the tasks run by {@link #executor} - * can depend on the results of the tasks run by {@link #raceDetailsExecutor}, and an {@link Executor} doesn't - * move a task that is blocked by waiting for another {@link FutureTask} to the side but blocks permanently, - * ending in a deadlock (one that cannot easily be detected by the Eclipse debugger either). - */ - private transient Executor raceDetailsExecutor; - - /** - * Used to remove all these listeners from their tracked races when this servlet is {@link #destroy() destroyed}. - */ - private transient Set cacheInvalidationListeners; - - private transient ThreadPoolExecutor executor; - - private transient LiveLeaderboardUpdater liveLeaderboardUpdater; - - private transient LeaderboardDTOCache leaderboardDTOCache; - - /** - * A leaderboard entry representing a snapshot of a cell at a given time point for a single race/competitor. - * - * @author Axel Uhl (d043530) - * - */ - class EntryImpl implements Entry { - private final Callable trackedRankProvider; - private final Double netPoints; - private final Callable netPointsUncorrectedProvider; - private final boolean isNetPointsCorrected; - private final Double totalPoints; - private final MaxPointsReason maxPointsReason; - private final boolean discarded; - private final Fleet fleet; - - private EntryImpl(Callable trackedRankProvider, Double netPoints, - Callable netPointsUncorrectedProvider, boolean isNetPointsCorrected, - Double totalPoints, MaxPointsReason maxPointsReason, boolean discarded, Fleet fleet) { - super(); - this.trackedRankProvider = trackedRankProvider; - this.netPoints = netPoints; - this.netPointsUncorrectedProvider = netPointsUncorrectedProvider; - this.isNetPointsCorrected = isNetPointsCorrected; - this.totalPoints = totalPoints; - this.maxPointsReason = maxPointsReason; - this.discarded = discarded; - this.fleet = fleet; - } - @Override - public int getTrackedRank() { - try { - return trackedRankProvider.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - @Override - public Double getNetPoints() { - return netPoints; - } - @Override - public boolean isNetPointsCorrected() { - return isNetPointsCorrected; - } - @Override - public Double getTotalPoints() { - return totalPoints; - } - @Override - public MaxPointsReason getMaxPointsReason() { - return maxPointsReason; - } - @Override - public boolean isDiscarded() { - return discarded; - } - @Override - public Fleet getFleet() { - return fleet; - } - @Override - public Double getNetPointsUncorrected() { - try { - return netPointsUncorrectedProvider.call(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - - /** - * Computing the competitors can be a bit expensive, particularly if the fleet is large and there may be suppressed - * competitors, and the leaderboard may be a meta-leaderboard that refers to other leaderboards which each have - * several tracked races attached from where the competitors need to be retrieved. Ideally, the competitors list - * would be cached, but that is again difficult because we would have to monitor all changes in all dependent - * leaderboards and columns and tracked races properly. - *

    - * - * As it turns out, one of the most frequent uses of the {@link AbstractSimpleLeaderboardImpl#getCompetitors} - * competitors list is to determine their number which in turn is only required for high-point scoring systems and - * for computing the default score for penalties. Again, the most frequently used low-point family of scoring schemes - * does not require this number. Yet, the scoring scheme requires an argument for polymorphic use by those that - * need it. Instead of computing it for each call, this interface lets us defer the actual calculation until the - * point when it's really needed. Once asked, this object will cache the result. Therefore, a new one should be - * constructed each time the number shall be computed. - * - * @author Axel Uhl (D043530) - * - */ - public class NumberOfCompetitorsFetcherImpl implements NumberOfCompetitorsInLeaderboardFetcher { - private int numberOfCompetitors = -1; - - @Override - public int getNumberOfCompetitorsInLeaderboard() { - if (numberOfCompetitors == -1) { - numberOfCompetitors = Util.size(getCompetitors()); - } - return numberOfCompetitors; - } - } - - /** - * Handles the invalidation of the {@link SailingServiceImpl#raceDetailsAtEndOfTrackingCache} entries if the tracked - * race changes in any way. In particular, for {@link #statusChanged}, when the status changes away from LOADING, - * calculations may start or resume, making it necessary to clear the cache. - * - * @author Axel Uhl (D043530) - * - */ - private class CacheInvalidationListener extends AbstractRaceChangeListener { - private final TrackedRace trackedRace; - private final Competitor competitor; - - public CacheInvalidationListener(TrackedRace trackedRace, Competitor competitor) { - this.trackedRace = trackedRace; - this.competitor = competitor; - } - - public TrackedRace getTrackedRace() { - return trackedRace; - } - - public void removeFromTrackedRace() { - trackedRace.removeListener(this); - } - - private void invalidateCacheAndRemoveThisListenerFromTrackedRace() { - synchronized (raceDetailsAtEndOfTrackingCache) { - raceDetailsAtEndOfTrackingCache.remove(new com.sap.sse.common.Util.Pair(trackedRace, competitor)); - removeFromTrackedRace(); - } - } - - @Override - protected void defaultAction() { - invalidateCacheAndRemoveThisListenerFromTrackedRace(); - } - } - - private static class UUIDGenerator implements LeaderboardDTO.UUIDGenerator { - @Override - public String generateRandomUUID() { - return UUID.randomUUID().toString(); - } - } - - public AbstractSimpleLeaderboardImpl(ThresholdBasedResultDiscardingRule resultDiscardingRule) { - this.carriedPoints = new HashMap(); - this.scoreCorrection = createScoreCorrection(); - this.displayNames = new HashMap(); - this.crossLeaderboardResultDiscardingRule = resultDiscardingRule; - this.suppressedCompetitors = new HashSet(); - this.suppressedCompetitorsLock = new NamedReentrantReadWriteLock("suppressedCompetitorsLock", /* fair */ false); - this.raceColumnListeners = new RaceColumnListeners(); - this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); - initTransientFields(); - } - - /** - * Produces the score correction object to use in this leaderboard. Used by the constructor. Subclasses may override - * this method to create a more specific type of score correction. This implementation produces an object of type - * {@link ScoreCorrectionImpl}. - */ - protected SettableScoreCorrection createScoreCorrection() { - return new ScoreCorrectionImpl(this); - } - - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - ois.defaultReadObject(); - initTransientFields(); - } - - private void initTransientFields() { - this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); - this.raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); - this.cacheInvalidationListeners = new HashSet(); - // When many updates are triggered in a short period of time by a single thread, ensure that the single thread - // providing the updates is not outperformed by all the re-calculations happening here. Leave at least one - // core to other things, but by using at least three threads ensure that no simplistic deadlocks may occur. - final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3); - executor = new ThreadPoolExecutor(/* corePoolSize */ THREAD_POOL_SIZE, - /* maximumPoolSize */ THREAD_POOL_SIZE, - /* keepAliveTime */ 60, TimeUnit.SECONDS, - /* workQueue */ new LinkedBlockingQueue(), new ThreadFactoryWithPriority(Thread.NORM_PRIORITY-1, /* daemon */ true)); - } - - @Override - public void destroy() { - for (CacheInvalidationListener cacheInvalidationListener : cacheInvalidationListeners) { - cacheInvalidationListener.removeFromTrackedRace(); - } - } - - @Override - public SettableScoreCorrection getScoreCorrection() { - return scoreCorrection; - } - - @Override - public String getDisplayName(Competitor competitor) { - return displayNames.get(competitor); - } - - @Override - public String getDisplayName() { - return displayName; - } - - @Override - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - @Override - public ResultDiscardingRule getResultDiscardingRule() { - return crossLeaderboardResultDiscardingRule; - } - - @Override - public void setCarriedPoints(Competitor competitor, double carriedPoints) { - Double oldCarriedPoints = this.carriedPoints.put(competitor, carriedPoints); - if (!Util.equalsWithNull(oldCarriedPoints, carriedPoints)) { - getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, carriedPoints); - } - } - - @Override - public double getCarriedPoints(Competitor competitor) { - Double result = carriedPoints.get(competitor); - return result == null ? 0 : result; - } - - @Override - public Map getCompetitorsForWhichThereAreCarriedPoints() { - return Collections.unmodifiableMap(carriedPoints); - } - - @Override - public void unsetCarriedPoints(Competitor competitor) { - Double oldCarriedPoints = carriedPoints.remove(competitor); - if (oldCarriedPoints != null) { - getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, null); - } - } - - @Override - public boolean hasCarriedPoints() { - return !carriedPoints.isEmpty(); - } - - @Override - public boolean hasCarriedPoints(Competitor competitor) { - return carriedPoints.containsKey(competitor); - } - - @Override - public void setDisplayName(Competitor competitor, String displayName) { - String oldDisplayName = displayNames.get(competitor); - displayNames.put(competitor, displayName); - if (!Util.equalsWithNull(oldDisplayName, displayName)) { - getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); - } - } - - @Override - public void setCrossLeaderboardResultDiscardingRule(ThresholdBasedResultDiscardingRule discardingRule) { - ResultDiscardingRule oldDiscardingRule = getResultDiscardingRule(); - this.crossLeaderboardResultDiscardingRule = discardingRule; - getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, discardingRule); - } - - @Override - public Double getNetPoints(final Competitor competitor, final RaceColumn raceColumn, final TimePoint timePoint) { - return getScoreCorrection().getCorrectedScore( - new Callable() { - public Integer call() throws NoWindException { - return getTrackedRank(competitor, raceColumn, timePoint); - } - }, competitor, - raceColumn, timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()).getCorrectedScore(); - } - - @Override - public MaxPointsReason getMaxPointsReason(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - return getScoreCorrection().getMaxPointsReason(competitor, raceColumn, timePoint); - } - - @Override - public boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - return isDiscarded(competitor, raceColumn, getRaceColumns(), timePoint); - } - - private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, - Iterable raceColumnsToConsider, TimePoint timePoint) { - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - return isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns); - } - - /** - * Same as {@link #isDiscarded(Competitor, RaceColumn, TimePoint)}, only that the set of discarded race columns can - * be specified which is useful when total points are to be computed for more than one column for the same - * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done - * once and not again for each column (which would lead to quadratic effort). - * - * @param discardedRaceColumns - * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. - * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) - * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. - */ - private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, - final Set discardedRaceColumns) { - return !raceColumn.isMedalRace() - && getMaxPointsReason(competitor, raceColumn, timePoint).isDiscardable() - && discardedRaceColumns.contains(raceColumn); - } - - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { - return getTotalPoints(competitor, raceColumn, getRaceColumns(), timePoint); - } - - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, - Iterable raceColumnsToConsider, TimePoint timePoint) throws NoWindException { - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - return getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); - } - - /** - * Same as {@link #getTotalPoints(Competitor, RaceColumn, Iterable, TimePoint)}, only that the set of discarded race columns can - * be specified which is useful when total points are to be computed for more than one column for the same - * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done - * once and not again for each column (which would lead to quadratic effort). - * - * @param discardedRaceColumns - * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. - * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) - * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. - */ - @Override - public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, Set discardedRaceColumns) { - Double result; - if (isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns)) { - result = 0.0; - } else { - final Double netPoints = getNetPoints(competitor, raceColumn, timePoint); - if (netPoints == null) { - result = null; - } else { - result = raceColumn.getFactor() * netPoints; - } - } - return result; - } - - @Override - public Double getTotalPoints(Competitor competitor, TimePoint timePoint) { - return getTotalPoints(competitor, getRaceColumns(), timePoint); - } - - @Override - public Double getTotalPoints(Competitor competitor, final Iterable raceColumnsToConsider, - TimePoint timePoint) { - // when a column with isStartsWithZeroScore() is found, only reset score if the competitor scored in any race from there on - boolean needToResetScoreUponNextNonEmptyEntry = false; - double result = getCarriedPoints(competitor); - final Set discardedRaceColumns = getResultDiscardingRule() - .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); - for (RaceColumn raceColumn : raceColumnsToConsider) { - if (raceColumn.isStartsWithZeroScore()) { - needToResetScoreUponNextNonEmptyEntry = true; - } - if (getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint)) { - final Double totalPoints = getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); - if (totalPoints != null) { - if (needToResetScoreUponNextNonEmptyEntry) { - result = 0; - needToResetScoreUponNextNonEmptyEntry = false; - } - result += totalPoints; - } - } - } - return result; - } - - /** - * All competitors with non-null net points are added to the result which is then sorted by net points in ascending - * order. The fleet, if ordered, is the primary ordering criterion, followed by the net points. - */ - @Override - public List getCompetitorsFromBestToWorst(final RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { - final Map> netPointsAndFleet = new HashMap>(); - for (Competitor competitor : getCompetitors()) { - Double netPoints = getNetPoints(competitor, raceColumn, timePoint); - if (netPoints != null) { - netPointsAndFleet.put(competitor, new com.sap.sse.common.Util.Pair(netPoints, raceColumn.getFleetOfCompetitor(competitor))); - } - } - List result = new ArrayList(netPointsAndFleet.keySet()); - Collections.sort(result, new Comparator() { - @Override - public int compare(Competitor o1, Competitor o2) { - int comparisonResult; - if (o1 == o2) { - comparisonResult = 0; - } else { - if (raceColumn.hasSplitFleets() && !raceColumn.hasSplitFleetContiguousScoring()) { - // only check fleets if there are more than one and the column is not to be contiguously scored even in case - // of split fleets - final Fleet o1Fleet = netPointsAndFleet.get(o1).getB(); - final Fleet o2Fleet = netPointsAndFleet.get(o2).getB(); - if (o1Fleet == null) { - if (o2Fleet == null) { - comparisonResult = 0; - } else { - comparisonResult = 1; // o1 ranks "worse" because it doesn't have a fleet set while o2 - // has - } - } else { - if (o2Fleet == null) { - comparisonResult = -1; // o1 ranks "better" because it has a fleet set while o2 hasn't - } else { - comparisonResult = o1Fleet.compareTo(o2Fleet); - } - } - } else { - // either there are no split fleets or the split isn't relevant for scoring as for ordered fleets - // the scoring runs contiguously from top to bottom - comparisonResult = 0; - } - if (comparisonResult == 0) { - comparisonResult = getScoringScheme().getScoreComparator(/* nullScoresAreBetter */ false).compare( - netPointsAndFleet.get(o1).getA(), netPointsAndFleet.get(o2).getA()); - } - } - return comparisonResult; - } - }); - return result; - } - - @Override - public List getCompetitorsFromBestToWorst(TimePoint timePoint) { - return getCompetitorsFromBestToWorst(getRaceColumns(), timePoint); - } - - private List getCompetitorsFromBestToWorst(Iterable raceColumnsToConsider, TimePoint timePoint) { - List result = new ArrayList(); - for (Competitor competitor : getCompetitors()) { - result.add(competitor); - } - Collections.sort(result, getTotalRankComparator(raceColumnsToConsider, timePoint)); - return result; - } - - @Override - public int getTotalRankOfCompetitor(Competitor competitor, TimePoint timePoint) throws NoWindException { - List competitorsFromBestToWorst = getCompetitorsFromBestToWorst(timePoint); - return competitorsFromBestToWorst.indexOf(competitor) + 1; - } - - protected Comparator getTotalRankComparator(Iterable raceColumnsToConsider, TimePoint timePoint) { - return new LeaderboardTotalRankComparator(this, timePoint, getScoringScheme(), /* nullScoresAreBetter */ false, raceColumnsToConsider); - } - - @Override - public RaceColumn getRaceColumnByName(String columnName) { - RaceColumn result = null; - for (RaceColumn r : getRaceColumns()) { - if (r.getName().equals(columnName)) { - result = r; - break; - } - } - return result; - } - - @Override - public Competitor getCompetitorByName(String competitorName) { - for (Competitor competitor : getAllCompetitors()) { - if (competitor.getName().equals(competitorName)) { - return competitor; - } - } - return null; - } - - @Override - public boolean countRaceForComparisonWithDiscardingThresholds(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - TrackedRace trackedRaceForCompetitorInColumn; - return getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint) && - (getScoreCorrection().isScoreCorrected(competitor, raceColumn, timePoint) || - ((trackedRaceForCompetitorInColumn=raceColumn.getTrackedRace(competitor)) != null && - trackedRaceForCompetitorInColumn.hasStarted(timePoint) && - trackedRaceForCompetitorInColumn.getRank(competitor, timePoint) != 0)); - } - - @Override - public void addRaceColumnListener(RaceColumnListener listener) { - getRaceColumnListeners().addRaceColumnListener(listener); - } - - @Override - public void removeRaceColumnListener(RaceColumnListener listener) { - getRaceColumnListeners().removeRaceColumnListener(listener); - } - - @Override - public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint) throws NoWindException { - final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - return getEntry(competitor, race, timePoint, discardedRaceColumns); - } - - @Override - public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint, - Set discardedRaceColumns) throws NoWindException { - Callable trackedRankProvider = new Callable() { - public Integer call() throws NoWindException { - return getTrackedRank(competitor, race, timePoint); - } - }; - final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, race, - timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); - boolean discarded = isDiscarded(competitor, race, timePoint, discardedRaceColumns); - final Double correctedScore = correctedResults.getCorrectedScore(); - return new EntryImpl(trackedRankProvider, correctedScore, new Callable() { - @Override - public Double call() { - return correctedResults.getUncorrectedScore(); - } - }, correctedResults.isCorrected(), discarded ? DOUBLE_0 : correctedScore == null ? null - : Double.valueOf(correctedScore * race.getFactor()), correctedResults.getMaxPointsReason(), discarded, - race.getFleetOfCompetitor(competitor)); - } - - @Override - public Map> getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(TimePoint timePoint) throws NoWindException { - Map> result = new LinkedHashMap<>(); - List raceColumnsToConsider = new ArrayList<>(); - for (RaceColumn raceColumn : getRaceColumns()) { - raceColumnsToConsider.add(raceColumn); - result.put(raceColumn, getCompetitorsFromBestToWorst(raceColumnsToConsider, timePoint)); - } - return result; - } - - @Override - public Map> getTotalPointsSumAfterRaceColumn(final TimePoint timePoint) - throws NoWindException { - final Map> result = new LinkedHashMap<>(); - List raceColumnsToConsider = new ArrayList<>(); - Map>> futures = new HashMap<>(); - for (final RaceColumn raceColumn : getRaceColumns()) { - raceColumnsToConsider.add(raceColumn); - final Iterable finalRaceColumnsToConsider = new ArrayList<>(raceColumnsToConsider); - futures.put(raceColumn, executor.submit(new Callable>() { - @Override - public Map call() { - Map totalPointsSumPerCompetitorInColumn = new HashMap<>(); - for (Competitor competitor : getCompetitors()) { - totalPointsSumPerCompetitorInColumn.put(competitor, getTotalPoints(competitor, finalRaceColumnsToConsider, timePoint)); - } - synchronized (result) { - return totalPointsSumPerCompetitorInColumn; - } - } - })); - } - for (RaceColumn raceColumn : getRaceColumns()) { - try { - result.put(raceColumn, futures.get(raceColumn).get()); - } catch (InterruptedException | ExecutionException e) { - if (e.getCause() instanceof NoWindError) { - throw ((NoWindError) e.getCause()).getCause(); - } else { - throw new RuntimeException(e); // no caught exceptions occur in the futures executed - } - } - } - return result; - } - - @Override - public Map, Entry> getContent(final TimePoint timePoint) throws NoWindException { - Map, Entry> result = new HashMap, Entry>(); - Map> discardedRaces = new HashMap>(); - for (final RaceColumn raceColumn : getRaceColumns()) { - for (final Competitor competitor : getCompetitors()) { - Callable trackedRankProvider = new Callable() { - @Override - public Integer call() throws Exception { - return getTrackedRank(competitor, raceColumn, timePoint); - } - }; - final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, raceColumn, - timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); - Set discardedRacesForCompetitor = discardedRaces.get(competitor); - if (discardedRacesForCompetitor == null) { - discardedRacesForCompetitor = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - discardedRaces.put(competitor, discardedRacesForCompetitor); - } - boolean discarded = discardedRacesForCompetitor.contains(raceColumn); - final Double correctedScore = correctedResults.getCorrectedScore(); - Entry entry = new EntryImpl(trackedRankProvider, correctedScore, - new Callable() { @Override public Double call() { return correctedResults.getUncorrectedScore(); } }, - correctedResults.isCorrected(), - discarded ? DOUBLE_0 : (correctedScore==null?null: - Double.valueOf((correctedScore * raceColumn.getFactor()))), correctedResults.getMaxPointsReason(), - discarded, raceColumn.getFleetOfCompetitor(competitor)); - result.put(new com.sap.sse.common.Util.Pair(competitor, raceColumn), entry); - } - } - return result; - } - - @Override - public void trackedRaceLinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); - } - - @Override - public void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); - // It's generally possible that a leaderboard links to the same tracked race in multiple columns / fleets; - // only if it no longer references the trackedRace currently unlinked from one column/fleet, also unlink - // all cache invalidation listeners for said trackedRace - if (!Util.contains(getTrackedRaces(), trackedRace)) { - synchronized (cacheInvalidationListeners) { - for (Iterator cacheInvalidationListenerIter=cacheInvalidationListeners.iterator(); - cacheInvalidationListenerIter.hasNext(); ) { - CacheInvalidationListener cacheInvalidationListener = cacheInvalidationListenerIter.next(); - if (cacheInvalidationListener.getTrackedRace() == trackedRace) { - cacheInvalidationListener.removeFromTrackedRace(); - cacheInvalidationListenerIter.remove(); - } - } - } - } - } - - @Override - public void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { - getRaceColumnListeners().notifyListenersAboutIsMedalRaceChanged(raceColumn, newIsMedalRace); - } - - @Override - public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { - getRaceColumnListeners().notifyListenersAboutIsStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); - } - - @Override - public void isFirstColumnIsNonDiscardableCarryForwardChanged(RaceColumn raceColumn, boolean firstColumnIsNonDiscardableCarryForward) { - getRaceColumnListeners().notifyListenersAboutIsFirstColumnIsNonDiscardableCarryForwardChanged(raceColumn, firstColumnIsNonDiscardableCarryForward); - } - - @Override - public void hasSplitFleetContiguousScoringChanged(RaceColumn raceColumn, boolean hasSplitFleetContiguousScoring) { - getRaceColumnListeners().notifyListenersAboutHasSplitFleetContiguousScoringChanged(raceColumn, hasSplitFleetContiguousScoring); - } - - @Override - public void raceColumnMoved(RaceColumn raceColumn, int newIndex) { - getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(raceColumn, newIndex); - } - - @Override - public void factorChanged(RaceColumn raceColumn, Double oldFactor, Double newFactor) { - getRaceColumnListeners().notifyListenersAboutFactorChanged(raceColumn, oldFactor, newFactor); - } - - /** - * A leaderboard will only accept the addition of a race column if the column's name is unique across the leaderboard. - */ - @Override - public boolean canAddRaceColumnToContainer(RaceColumn newRaceColumn) { - boolean result = true; - for (RaceColumn raceColumn : getRaceColumns()) { - if (raceColumn.getName().equals(newRaceColumn.getName())) { - result = false; - break; - } - } - return result; - } - - @Override - public void raceColumnAddedToContainer(RaceColumn raceColumn) { - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(raceColumn); - } - - @Override - public void raceColumnRemovedFromContainer(RaceColumn raceColumn) { - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); - } - - @Override - public void competitorDisplayNameChanged(Competitor competitor, String oldDisplayName, String displayName) { - getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); - } - - @Override - public void resultDiscardingRuleChanged(ResultDiscardingRule oldDiscardingRule, ResultDiscardingRule newDiscardingRule) { - getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, newDiscardingRule); - } - - @Override - public boolean isTransient() { - return false; - } - - /** - * Finds out the time point when any of the {@link Leaderboard#getTrackedRaces() tracked races currently attached to - * the leaderboard} and the {@link Leaderboard#getScoreCorrection() score corrections} have last been - * modified. If no tracked race is attached and no time-stamped score corrections have been applied to the leaderboard, - * null is returned. The time point computed this way is a good choice for normalizing queries for later time - * points in an attempt to achieve more cache hits.

    - * - * Note, however, that the result does not tell about structural changes to the leaderboard and therefore cannot be used - * to determine the need for cache invalidation. For example, if a column is added to a leaderboard after the time point - * returned by this method but that column's attached tracked race has finished before the time point returned by this method, - * the result of this method won't change. Still, the contents of the leaderboard will change by a change in column structure. - * A different means to determine the possibility of changes that happened to this leaderboard must be used for cache - * management. Such a facility has to listen for score correction changes, tracked races being attached or detached and - * the column structure changing. - * - * @see TrackedRace#getTimePointOfNewestEvent() - * @see SettableScoreCorrection#getTimePointOfLastCorrectionsValidity() - */ - @Override - public TimePoint getTimePointOfLatestModification() { - TimePoint result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - if (result == null || (trackedRace.getTimePointOfNewestEvent() != null && trackedRace.getTimePointOfNewestEvent().after(result))) { - result = trackedRace.getTimePointOfNewestEvent(); - } - } - TimePoint timePointOfLastScoreCorrection = getScoreCorrection().getTimePointOfLastCorrectionsValidity(); - if (timePointOfLastScoreCorrection != null && (result == null || timePointOfLastScoreCorrection.after(result))) { - result = timePointOfLastScoreCorrection; - } - return result; - } - - @Override - public com.sap.sse.common.Util.Pair getMaximumSpeedOverGround(Competitor competitor, TimePoint timePoint) { - com.sap.sse.common.Util.Pair result = null; - // TODO should we ensure that competitor participated in all race columns? - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = markPassings.first().getTimePoint(); - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) && - markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - to = timePoint; - } - com.sap.sse.common.Util.Pair maxSpeed = trackedRace.getTrack(competitor).getMaximumSpeedOverGround(from, to); - if (result == null || result.getB() == null || - (maxSpeed != null && maxSpeed.getB() != null && maxSpeed.getB().compareTo(result.getB()) > 0)) { - result = maxSpeed; - } - } - } - } - return result; - } - - @Override - public Speed getAverageSpeedOverGround(Competitor competitor, TimePoint timePoint) { - Speed result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = markPassings.first().getTimePoint(); - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) && - markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - if (markPassings.last().getWaypoint() != trackedRace.getRace().getCourse().getLastWaypoint() && - timePoint.after(markPassings.last().getTimePoint())) { - result = null; - break; - } - to = timePoint; - } - Distance distanceTraveled = trackedRace.getDistanceTraveled(competitor, timePoint); - if (distanceTraveled != null) { - result = distanceTraveled.inTime(to.asMillis()-from.asMillis()); - } - } - } - } - return result; - } - - @Override - public Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint) throws NoWindException { - return getTotalTimeSailedInLegType(competitor, legType, timePoint, new HashMap()); - } - - private Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint, Map legTypeCache) throws NoWindException { - Duration result = null; - // TODO should we ensure that competitor participated in all race columns? - outerLoop: - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - trackedRace.getRace().getCourse().lockForRead(); - try { - for (Leg leg : trackedRace.getRace().getCourse().getLegs()) { - TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, leg); - if (trackedLegOfCompetitor.hasStartedLeg(timePoint)) { - // find out leg type at the time the competitor started the leg - try { - final TrackedLeg trackedLeg = trackedRace.getTrackedLeg(leg); - LegType trackedLegType = legTypeCache.get(trackedLeg); - if (trackedLegType == null) { - final TimePoint startTime = trackedLegOfCompetitor.getStartTime(); - TimePoint finishTime = trackedLegOfCompetitor.getFinishTime(); - if (finishTime == null) { - finishTime = timePoint; - } - trackedLegType = trackedLeg.getLegType(startTime.plus(startTime.until(finishTime).divide(2))); // middle of the leg - legTypeCache.put(trackedLeg, trackedLegType); - } - if (legType == trackedLegType) { - Duration timeSpentOnDownwind = trackedLegOfCompetitor.getTime(timePoint); - if (timeSpentOnDownwind != null) { - if (result == null) { - result = timeSpentOnDownwind; - } else { - result = result.plus(timeSpentOnDownwind); - } - } else { - // Although the competitor has started the leg, no value was produced. This - // means that the competitor didn't finish the leg before tracking ended. No useful value - // can be obtained for this competitor anymore. - result = null; - break outerLoop; - } - } - } catch (NoWindException nwe) { - // without wind there is no leg type and hence there is no reasonable value for this: - result = null; - break outerLoop; - } - } - } - } finally { - trackedRace.getRace().getCourse().unlockAfterRead(); - } - } - } - return result; - } - - @Override - public Duration getTotalTimeSailed(Competitor competitor, TimePoint timePoint) { - Duration result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = trackedRace.getStartOfRace(); // start counting at race start, not when the competitor passed the line - if (from != null && !timePoint.before(from)) { // but only if the race started after timePoint - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) - && markPassings.last().getWaypoint() == trackedRace.getRace().getCourse() - .getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - if (trackedRace.getEndOfTracking() != null - && timePoint.after(trackedRace.getEndOfTracking())) { - result = null; // race not finished until end of tracking; no reasonable value can be - // computed for competitor - break; - } else { - to = timePoint; - } - } - Duration timeSpent = from.until(to); - if (result == null) { - result = timeSpent; - } else { - result=result.plus(timeSpent); - } - } - } - } - } - return result; - } - - @Override - public Distance getTotalDistanceTraveled(Competitor competitor, TimePoint timePoint) { - Distance result = null; - for (TrackedRace trackedRace : getTrackedRaces()) { - TimePoint startOfRace; - if (Util.contains(trackedRace.getRace().getCompetitors(), competitor) && - (startOfRace=trackedRace.getStartOfRace()) != null && - !startOfRace.after(timePoint)) { - Distance distanceSailedInRace = trackedRace.getDistanceTraveled(competitor, timePoint); - if (distanceSailedInRace != null) { - if (result == null) { - result = distanceSailedInRace; - } else { - result = result.add(distanceSailedInRace); - } - } else { - // if competitor has not finished one single race in the whole - // series then we can not return a meaningful value for all - // all races - return null; - } - } - } - return result; - } - - protected RaceColumnListeners getRaceColumnListeners() { - return raceColumnListeners; - } - - @Override - public Iterable getCompetitors() { - final Iterable result; - // mostly the set of suppressed competitors is empty; in this case, avoid having to loop over the - // potentially large set of competitors - if (Util.isEmpty(getSuppressedCompetitors())) { - result = getAllCompetitors(); - } else { - final Iterable allCompetitors = getAllCompetitors(); - final Set suppressed = new HashSet<>(); - Util.addAll(getSuppressedCompetitors(), suppressed); - result = getCompetitorIterableSkippingSuppressed(allCompetitors, suppressed); - } - return result; - } - - @Override - public Iterable getCompetitors(RaceColumn raceColumn, Fleet fleet) { - return getCompetitorIterableSkippingSuppressed(getAllCompetitors(raceColumn, fleet), getSuppressedCompetitors()); - } - - /** - * return an iterable with a smart iterator that filters out the suppressed elements on demand - */ - protected Iterable getCompetitorIterableSkippingSuppressed(final Iterable allCompetitors, - final Iterable suppressed) { - final Iterable result; - result = new Iterable() { - @Override - public Iterator iterator() { - return new Iterator() { - private final Iterator allIter = allCompetitors.iterator(); - private Competitor next = advance(); - - private Competitor advance() { - next = null; - while (allIter.hasNext() && next == null) { - next = allIter.next(); - if (Util.contains(suppressed, next)) { - next = null; - } - } - return next; - } - - @Override - public boolean hasNext() { - return next != null; - } - - @Override - public Competitor next() { - if (next == null) { - throw new NoSuchElementException(); - } - final Competitor result = next; - advance(); - return result; - } - }; - } - }; - return result; - } - - @Override - public Iterable getSuppressedCompetitors() { - LockUtil.lockForRead(suppressedCompetitorsLock); - try { - return new HashSet(suppressedCompetitors); - } finally { - LockUtil.unlockAfterRead(suppressedCompetitorsLock); - } - } - - @Override - public void setSuppressed(Competitor competitor, boolean suppressed) { - LockUtil.lockForWrite(suppressedCompetitorsLock); - try { - if (suppressed) { - suppressedCompetitors.add(competitor); - } else { - suppressedCompetitors.remove(competitor); - } - } finally { - LockUtil.unlockAfterWrite(suppressedCompetitorsLock); - } - getScoreCorrection().notifyListenersAboutIsSuppressedChange(competitor, suppressed); - } - - @Override - public TimePoint getNowMinusDelay() { - final TimePoint now = MillisecondsTimePoint.now(); - final Long delayToLiveInMillis = getDelayToLiveInMillis(); - TimePoint timePoint = delayToLiveInMillis == null ? now : now.minus(delayToLiveInMillis); - return timePoint; - } - - @Override - public void raceLogEventAdded(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { - getRaceColumnListeners().notifyListenersAboutRaceLogEventAdded(raceColumn, raceLogIdentifier, event); - if (event instanceof InvalidatesLeaderboardCache) { - // make sure to invalidate the cache as this event indicates that - // it changes values the cache could still hold - if (leaderboardDTOCache != null) { - leaderboardDTOCache.invalidate(this); - } - } - } - - @Override - public LeaderboardDTO computeDTO(final TimePoint timePoint, - final Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, - final boolean waitForLatestAnalyses, TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory, - final boolean fillNetPointsUncorrected) - throws NoWindException { - long startOfRequestHandling = System.currentTimeMillis(); - final LeaderboardDTOCalculationReuseCache cache = new LeaderboardDTOCalculationReuseCache(timePoint); - final LeaderboardDTO result = new LeaderboardDTO(this.getScoreCorrection().getTimePointOfLastCorrectionsValidity() == null ? null - : this.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(), - this.getScoreCorrection() == null ? null : this.getScoreCorrection().getComment(), - this.getScoringScheme() == null ? null : this.getScoringScheme().getType(), this - .getScoringScheme().isHigherBetter(), new UUIDGenerator(), addOverallDetails); - result.competitors = new ArrayList(); - result.name = this.getName(); - result.competitorDisplayNames = new HashMap(); - for (Competitor suppressedCompetitor : this.getSuppressedCompetitors()) { - result.setSuppressed(baseDomainFactory.convertToCompetitorDTO(suppressedCompetitor), true); - } - // Now create the race columns and, as a future task, set their competitorsFromBestToWorst, then wait for all these - // futures to finish: - Map>> competitorsFromBestToWorstTasks = new HashMap<>(); - for (final RaceColumn raceColumn : this.getRaceColumns()) { - result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(), - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null); - for (Fleet fleet : raceColumn.getFleets()) { - RegattaAndRaceIdentifier raceIdentifier = null; - RaceDTO race = null; - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - final FleetDTO fleetDTO = baseDomainFactory.convertToFleetDTO(fleet); - if (trackedRace != null) { - raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta().getName(), - trackedRace.getRace().getName()); - race = baseDomainFactory.createRaceDTO(trackedRegattaRegistry, /* withGeoLocationData */ false, raceIdentifier, trackedRace); - } - // Note: the RaceColumnDTO won't be created by the following addRace call because it has been created - // above by the result.createEmptyRaceColumn call - result.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, - raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, - fleetDTO, raceColumn.isMedalRace(), raceIdentifier, race); - } - FutureTask> task = new FutureTask>( - () -> baseDomainFactory.getCompetitorDTOList(AbstractSimpleLeaderboardImpl.this.getCompetitorsFromBestToWorst(raceColumn, timePoint))); - executor.execute(task); - competitorsFromBestToWorstTasks.put(raceColumn, task); - } - // wait for the competitor orderings to have been computed for all race columns before continuing; subsequent tasks may depend on these data - for (Map.Entry>> raceColumnAndTaskToJoin : competitorsFromBestToWorstTasks.entrySet()) { - try { - result.setCompetitorsFromBestToWorst(raceColumnAndTaskToJoin.getKey().getName(), raceColumnAndTaskToJoin.getValue().get()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept - // null values. - // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So - // this helps robustness from a user's perspective. - logger.log( - Level.SEVERE, - AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " - + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails - + "): exception during computing competitor ordering for race column "+raceColumnAndTaskToJoin.getKey().getName(), e); - } - } - result.setDelayToLiveInMillisForLatestRace(this.getDelayToLiveInMillis()); - result.rows = new HashMap(); - result.hasCarriedPoints = this.hasCarriedPoints(); - if (this.getResultDiscardingRule() instanceof ThresholdBasedResultDiscardingRule) { - result.discardThresholds = ((ThresholdBasedResultDiscardingRule) this.getResultDiscardingRule()) - .getDiscardIndexResultsStartingWithHowManyRaces(); - } else { - result.discardThresholds = null; - } - // Computing the competitor leg ranks is expensive, especially in live mode, in case new events keep - // invalidating the ranks cache in TrackedLegImpl. The problem then is that the sorting based on wind data is repeated for - // each competitor, leading to square effort. We therefore need to compute the leg ranks for those races where leg - // details are requested only once and pass them into getLeaderboardEntryDTO - final Map> legRanksCache = new HashMap>(); - for (final RaceColumn raceColumn : this.getRaceColumns()) { - // if details for the column are requested, cache the leg's ranks - if (namesOfRaceColumnsForWhichToLoadLegDetails != null - && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName())) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - trackedRace.getRace().getCourse().lockForRead(); - try { - for (TrackedLeg trackedLeg : trackedRace.getTrackedLegs()) { - legRanksCache.put(trackedLeg.getLeg(), trackedLeg.getRanks(timePoint, cache)); - } - } finally { - trackedRace.getRace().getCourse().unlockAfterRead(); - } - } - } - } - } - for (final Competitor competitor : this.getCompetitorsFromBestToWorst(timePoint)) { - CompetitorDTO competitorDTO = baseDomainFactory.convertToCompetitorDTO(competitor); - LeaderboardRowDTO row = new LeaderboardRowDTO(); - row.competitor = competitorDTO; - row.fieldsByRaceColumnName = new HashMap(); - row.carriedPoints = this.hasCarriedPoints(competitor) ? this.getCarriedPoints(competitor) : null; - row.totalPoints = this.getTotalPoints(competitor, timePoint); - if (addOverallDetails) { - addOverallDetailsToRow(timePoint, competitor, row); - } - result.competitors.add(competitorDTO); - Map> futuresForColumnName = new HashMap>(); - final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); - for (final RaceColumn raceColumn : this.getRaceColumns()) { - RunnableFuture future = new FutureTask(() -> { - Entry entry = AbstractSimpleLeaderboardImpl.this.getEntry(competitor, raceColumn, timePoint, discardedRaceColumns); - return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails != null - && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn - .getName()), waitForLatestAnalyses, legRanksCache, baseDomainFactory, - fillNetPointsUncorrected, cache); - }); - executor.execute(future); - futuresForColumnName.put(raceColumn.getName(), future); - } - for (Map.Entry> raceColumnNameAndFuture : futuresForColumnName.entrySet()) { - try { - row.fieldsByRaceColumnName.put(raceColumnNameAndFuture.getKey(), raceColumnNameAndFuture.getValue().get()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept - // null values. - // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So - // this helps robustness from a user's perspective. - logger.log( - Level.SEVERE, - AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " - + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails - + "): exception during computing leaderboard entry for competitor " - + competitor.getName() + " in race column " + raceColumnNameAndFuture.getKey() - + ". Leaving empty.", e); - } - } - result.rows.put(competitorDTO, row); - String displayName = this.getDisplayName(competitor); - if (displayName != null) { - result.competitorDisplayNames.put(competitorDTO, displayName); - } - } - logger.info("computeLeaderboardByName(" + this.getName() + ", " + timePoint + ", " - + namesOfRaceColumnsForWhichToLoadLegDetails + ", addOverallDetails=" + addOverallDetails + ") took " - + (System.currentTimeMillis() - startOfRequestHandling) + "ms"); - return result; - } - - private void addOverallDetailsToRow(final TimePoint timePoint, - final Competitor competitor, LeaderboardRowDTO row) throws NoWindException { - final com.sap.sse.common.Util.Pair maximumSpeedOverGround = this.getMaximumSpeedOverGround(competitor, timePoint); - if (maximumSpeedOverGround != null && maximumSpeedOverGround.getB() != null) { - row.maximumSpeedOverGroundInKnots = maximumSpeedOverGround.getB().getKnots(); - row.whenMaximumSpeedOverGroundWasAchieved = maximumSpeedOverGround.getA().getTimePoint().asDate(); - } - Map legTypeCache = new HashMap<>(); - final Duration totalTimeSailedDownwind = this.getTotalTimeSailedInLegType(competitor, LegType.DOWNWIND, timePoint, legTypeCache); - row.totalTimeSailedDownwindInSeconds = totalTimeSailedDownwind==null?null:totalTimeSailedDownwind.asSeconds(); - final Duration totalTimeSailedUpwind = this.getTotalTimeSailedInLegType(competitor, LegType.UPWIND, timePoint, legTypeCache); - row.totalTimeSailedUpwindInSeconds = totalTimeSailedUpwind==null?null:totalTimeSailedUpwind.asSeconds(); - final Duration totalTimeSailedReaching = this.getTotalTimeSailedInLegType(competitor, LegType.REACHING, timePoint, legTypeCache); - row.totalTimeSailedReachingInSeconds = totalTimeSailedReaching==null?null:totalTimeSailedReaching.asSeconds(); - final Duration totalTimeSailed = this.getTotalTimeSailed(competitor, timePoint); - row.totalTimeSailedInSeconds = totalTimeSailed==null?null:totalTimeSailed.asSeconds(); - final Distance totalDistanceTraveledInMeters = this.getTotalDistanceTraveled(competitor, timePoint); - row.totalDistanceTraveledInMeters = totalDistanceTraveledInMeters==null?null:totalDistanceTraveledInMeters.getMeters(); - } - - /** - * @param waitForLatestAnalyses - * if false, this method is allowed to read the maneuver analysis results from a cache that - * may not reflect all data already received; otherwise, the method will always block for the latest - * cache updates to have happened before returning. - * @param fillNetPointsUncorrected - * tells if {@link LeaderboardEntryDTO#netPointsUncorrected} shall be filled; filling it is rather - * expensive, especially when compared to simply retrieving a score correction, and particularly if in a - * larger fleet a number of competitors haven't properly finished the race. This should only be used for - * leaderboard editing where a user needs to see what the uncorrected score was that would be used when - * the correction was removed. - */ - private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, RaceColumn raceColumn, Competitor competitor, - TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses, - Map> legRanksCache, DomainFactory baseDomainFactory, - boolean fillNetPointsUncorrected, WindLegTypeAndLegBearingCache cache) { - LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); - TrackedRace trackedRace = raceColumn.getTrackedRace(competitor); - entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); - entryDTO.netPoints = entry.getNetPoints(); - if (fillNetPointsUncorrected) { - entryDTO.netPointsUncorrected = entry.getNetPointsUncorrected(); - } - entryDTO.netPointsCorrected = entry.isNetPointsCorrected(); - entryDTO.totalPoints = entry.getTotalPoints(); - entryDTO.reasonForMaxPoints = entry.getMaxPointsReason(); - entryDTO.discarded = entry.isDiscarded(); - final GPSFixTrack track = trackedRace == null ? null : trackedRace.getTrack(competitor); - if (trackedRace != null) { - Date timePointOfLastPositionFixAtOrBeforeQueryTimePoint = getTimePointOfLastFixAtOrBefore(competitor, trackedRace, timePoint); - if (track != null) { - entryDTO.averageSamplingInterval = track.getAverageIntervalBetweenRawFixes(); - } - if (timePointOfLastPositionFixAtOrBeforeQueryTimePoint != null) { - long timeDifferenceInMs = timePoint.asMillis() - timePointOfLastPositionFixAtOrBeforeQueryTimePoint.getTime(); - entryDTO.timeSinceLastPositionFixInSeconds = timeDifferenceInMs == 0 ? 0.0 : timeDifferenceInMs / 1000.0; - } else { - entryDTO.timeSinceLastPositionFixInSeconds = null; - } - } - if (addLegDetails && trackedRace != null) { - try { - final RankingInfo rankingInfo = trackedRace.getRankingMetric().getRankingInfo(timePoint, cache); - RaceDetails raceDetails = getRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, - legRanksCache, rankingInfo, cache); - entryDTO.legDetails = raceDetails.getLegDetails(); - entryDTO.windwardDistanceToCompetitorFarthestAheadInMeters = raceDetails.getWindwardDistanceToCompetitorFarthestAhead() == null ? null - : raceDetails.getWindwardDistanceToCompetitorFarthestAhead().getMeters(); - entryDTO.gapToLeaderInOwnTime = trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache); - entryDTO.averageAbsoluteCrossTrackErrorInMeters = raceDetails.getAverageAbsoluteCrossTrackError() == null ? null - : raceDetails.getAverageAbsoluteCrossTrackError().getMeters(); - entryDTO.averageSignedCrossTrackErrorInMeters = raceDetails.getAverageSignedCrossTrackError() == null ? null - : raceDetails.getAverageSignedCrossTrackError().getMeters(); - entryDTO.calculatedTime = raceDetails.getCorrectedTime(); - entryDTO.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = raceDetails.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead(); - entryDTO.gapToLeaderInOwnTime = raceDetails.getGapToLeaderInOwnTime(); - final TimePoint startOfRace = trackedRace.getStartOfRace(); - if (startOfRace != null) { - Waypoint startWaypoint = trackedRace.getRace().getCourse().getFirstWaypoint(); - NavigableSet competitorMarkPassings = trackedRace.getMarkPassings(competitor); - trackedRace.lockForRead(competitorMarkPassings); - try { - if (!competitorMarkPassings.isEmpty()) { - final MarkPassing firstMarkPassing = competitorMarkPassings.iterator().next(); - if (firstMarkPassing.getWaypoint() == startWaypoint) { - Distance distanceToStartLineFiveSecondsBeforeStartOfRace = trackedRace.getDistanceToStartLine(competitor, /*milliseconds before start*/ 5000); - entryDTO.distanceToStartLineFiveSecondsBeforeStartInMeters = distanceToStartLineFiveSecondsBeforeStartOfRace == null ? null - : distanceToStartLineFiveSecondsBeforeStartOfRace.getMeters(); - Speed speedFiveSecondsBeforeStartOfRace = trackedRace.getSpeed(competitor, /*milliseconds before start*/ 5000); - entryDTO.speedOverGroundFiveSecondsBeforeStartInKnots = speedFiveSecondsBeforeStartOfRace == null ? null - : speedFiveSecondsBeforeStartOfRace.getKnots(); - Distance distanceToStartLineAtStartOfRace = trackedRace.getDistanceToStartLine( - competitor, startOfRace); - entryDTO.distanceToStartLineAtStartOfRaceInMeters = distanceToStartLineAtStartOfRace == null ? null - : distanceToStartLineAtStartOfRace.getMeters(); - Speed speedAtStartTime = track == null ? null : track.getEstimatedSpeed(startOfRace); - entryDTO.speedOverGroundAtStartOfRaceInKnots = speedAtStartTime == null ? null - : speedAtStartTime.getKnots(); - TimePoint competitorStartTime = firstMarkPassing.getTimePoint(); - entryDTO.timeBetweenRaceStartAndCompetitorStartInSeconds = startOfRace.until(competitorStartTime).asSeconds(); - Speed competitorSpeedWhenPassingStart = track == null ? null : track - .getEstimatedSpeed(competitorStartTime); - entryDTO.speedOverGroundAtPassingStartWaypointInKnots = competitorSpeedWhenPassingStart == null ? null - : competitorSpeedWhenPassingStart.getKnots(); - try { - entryDTO.startTack = trackedRace.getTack(competitor, competitorStartTime); - } catch (NoWindException nwe) { - entryDTO.startTack = null; // leave empty in case no wind information is available - } - Distance distanceFromStarboardSideOfStartLineWhenPassingStart = trackedRace - .getDistanceFromStarboardSideOfStartLineWhenPassingStart(competitor); - entryDTO.distanceToStarboardSideOfStartLineInMeters = distanceFromStarboardSideOfStartLineWhenPassingStart == null ? null - : distanceFromStarboardSideOfStartLineWhenPassingStart.getMeters(); - } - } - } finally { - trackedRace.unlockAfterRead(competitorMarkPassings); - } - } - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (ExecutionException e) { - throw new RuntimeException(e); // the future used to calculate the leg details was interrupted; escalate as runtime exception - } - } - final Fleet fleet = entry.getFleet(); - entryDTO.fleet = fleet == null ? null : baseDomainFactory.convertToFleetDTO(fleet); - return entryDTO; - } - - /** - * Determines the time point of the last raw fix (with outliers not removed) for competitor in - * trackedRace. If the competitor's track is null or empty, null is returned. - * @param trackedRace must not be null - * @param atOrBefore find the last fix at or before the time point specified - */ - private Date getTimePointOfLastFixAtOrBefore(Competitor competitor, TrackedRace trackedRace, TimePoint atOrBefore) { - assert trackedRace != null; - final Date timePointOfLastPositionFix; - GPSFixTrack track = trackedRace.getTrack(competitor); - if (track == null) { - timePointOfLastPositionFix = null; - } else { - GPSFixMoving lastFix = track.getLastFixAtOrBefore(atOrBefore); - if (lastFix == null) { - timePointOfLastPositionFix = null; - } else { - timePointOfLastPositionFix = lastFix.getTimePoint().asDate(); - } - } - return timePointOfLastPositionFix; - } - - private static class RaceDetails { - private final List legDetails; - private final Distance windwardDistanceToCompetitorFarthestAhead; - private final Distance averageAbsoluteCrossTrackError; - private final Distance averageSignedCrossTrackError; - private final Duration gapToLeaderInOwnTime; - private final Duration correctedTime; - private final Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - - public RaceDetails(List legDetails, Distance windwardDistanceToCompetitorFarthestAhead, - Distance averageAbsoluteCrossTrackError, Distance averageSignedCrossTrackError, - Duration gapToLeaderInOwnTime, Duration correctedTime, - Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { - super(); - this.legDetails = legDetails; - this.windwardDistanceToCompetitorFarthestAhead = windwardDistanceToCompetitorFarthestAhead; - this.averageAbsoluteCrossTrackError = averageAbsoluteCrossTrackError; - this.averageSignedCrossTrackError = averageSignedCrossTrackError; - this.gapToLeaderInOwnTime = gapToLeaderInOwnTime; - this.correctedTime = correctedTime; - this.correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - } - public List getLegDetails() { - return legDetails; - } - public Distance getWindwardDistanceToCompetitorFarthestAhead() { - return windwardDistanceToCompetitorFarthestAhead; - } - public Distance getAverageAbsoluteCrossTrackError() { - return averageAbsoluteCrossTrackError; - } - public Distance getAverageSignedCrossTrackError() { - return averageSignedCrossTrackError; - } - public Duration getGapToLeaderInOwnTime() { - return gapToLeaderInOwnTime; - } - public Duration getCorrectedTime() { - return correctedTime; - } - public Duration getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead() { - return correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; - } - } - - /** - * If timePoint is after the end of the race's tracking the query will be adjusted to obtain the values - * at the end of the {@link TrackedRace#getEndOfTracking() race's tracking time}. If the time point adjusted this - * way equals the end of the tracking time, the query results will be looked up in a cache first and if not found, - * they will be stored to the cache after calculating them. A cache invalidation {@link RaceChangeListener listener} - * will be registered with the race which will be triggered for any event received by the race. - * @param waitForLatestAnalyses - * if false, this method is allowed to read the maneuver analysis results from a cache that - * may not reflect all data already received; otherwise, the method will always block for the latest - * cache updates to have happened before returning. - */ - private RaceDetails getRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { - final RaceDetails raceDetails; - if (trackedRace.getEndOfTracking() != null && trackedRace.getEndOfTracking().compareTo(timePoint) < 0) { - raceDetails = getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(trackedRace, competitor, legRanksCache, rankingInfo, cache); - } else { - raceDetails = calculateRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache, cache, rankingInfo); - } - return raceDetails; - } - - private RaceDetails getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(final TrackedRace trackedRace, - final Competitor competitor, final Map> legRanksCache, - RankingInfo rankingInfo, final WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { - final com.sap.sse.common.Util.Pair key = new com.sap.sse.common.Util.Pair(trackedRace, competitor); - RunnableFuture raceDetails; - synchronized (raceDetailsAtEndOfTrackingCache) { - raceDetails = raceDetailsAtEndOfTrackingCache.get(key); - if (raceDetails == null) { - raceDetails = new FutureTask(new Callable() { - @Override - public RaceDetails call() throws Exception { - TimePoint end = trackedRace.getEndOfRace(); - if (end == null) { - end = trackedRace.getEndOfTracking(); - } - return calculateRaceDetails(trackedRace, competitor, end, - // TODO see bug 1358: for now, use waitForLatest==false until we've switched to optimistic locking for the course read lock - /* TODO old comment when it was still true: "because this is done only once after end of tracking" */ - /* waitForLatestAnalyses (maneuver and cross track error) */ false, - legRanksCache, cache, rankingInfo); - } - }); - raceDetailsExecutor.execute(raceDetails); - raceDetailsAtEndOfTrackingCache.put(key, raceDetails); - final CacheInvalidationListener cacheInvalidationListener = new CacheInvalidationListener(trackedRace, competitor); - trackedRace.addListener(cacheInvalidationListener); - cacheInvalidationListeners.add(cacheInvalidationListener); - } - } - return raceDetails.get(); - } - - private RaceDetails calculateRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - WindLegTypeAndLegBearingCache cache, final RankingInfo rankingInfo) { - final List legDetails = new ArrayList(); - final Course course = trackedRace.getRace().getCourse(); - course.lockForRead(); // hold back any course re-configurations while looping over the legs - try { - for (Leg leg : course.getLegs()) { - LegEntryDTO legEntry; - // We loop over a copy of the course's legs; during a course change, legs may become "stale," even with - // regard to the leg/trackedLeg structures inside the tracked race which is updated by the course change - // immediately. That's why we've acquired a read lock for the course above. - TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, leg); - if (trackedLeg != null && trackedLeg.hasStartedLeg(timePoint)) { - legEntry = createLegEntry(trackedLeg, timePoint, waitForLatestAnalyses, legRanksCache, rankingInfo, cache); - } else { - legEntry = null; - } - legDetails.add(legEntry); - } - final Distance windwardDistanceToCompetitorFarthestAhead = trackedRace == null ? null : trackedRace - .getWindwardDistanceToCompetitorFarthestAhead(competitor, timePoint, WindPositionMode.LEG_MIDDLE, rankingInfo, cache); - Distance averageAbsoluteCrossTrackError; - try { - averageAbsoluteCrossTrackError = trackedRace == null ? null : trackedRace.getAverageAbsoluteCrossTrackError( - competitor, timePoint, waitForLatestAnalyses, cache); - } catch (NoWindException nwe) { - // without wind information, use null meaning "unknown" - averageAbsoluteCrossTrackError = null; - } - Distance averageSignedCrossTrackError; - try { - averageSignedCrossTrackError = trackedRace == null ? null : trackedRace.getAverageSignedCrossTrackError( - competitor, timePoint, waitForLatestAnalyses, cache); - } catch (NoWindException nwe) { - // without wind information, use null meaning "unknown" - averageSignedCrossTrackError = null; - } - final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); - return new RaceDetails(legDetails, windwardDistanceToCompetitorFarthestAhead, averageAbsoluteCrossTrackError, averageSignedCrossTrackError, - trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache), - trackedRace.getRankingMetric().getCorrectedTime(competitor, timePoint), - competitorRankingInfo == null ? null : competitorRankingInfo.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead()); - } finally { - course.unlockAfterRead(); - } - } - - private LegEntryDTO createLegEntry(TrackedLegOfCompetitor trackedLeg, TimePoint timePoint, - boolean waitForLatestAnalyses, Map> legRanksCache, - RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { - LegEntryDTO result; - final Duration time = trackedLeg.getTime(timePoint); - if (trackedLeg == null || time == null) { - result = null; - } else { - result = new LegEntryDTO(); - try { - result.legType = trackedLeg.getTrackedLeg().getLegType(timePoint); - } catch (NoWindException nwe) { - result.legType = null; // can't determine leg type without wind data - } - final Speed averageSpeedOverGround = trackedLeg.getAverageSpeedOverGround(timePoint); - result.averageSpeedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); - Distance averageAbsoluteCrossTrackError; - try { - averageAbsoluteCrossTrackError = trackedLeg.getAverageAbsoluteCrossTrackError(timePoint, waitForLatestAnalyses); - } catch (NoWindException nwe) { - // leave averageAbsoluteCrossTrackError as null, meaning "unknown" - averageAbsoluteCrossTrackError = null; - } - result.averageAbsoluteCrossTrackErrorInMeters = averageAbsoluteCrossTrackError == null ? null : averageAbsoluteCrossTrackError.getMeters(); - Distance averageSignedCrossTrackError; - try { - averageSignedCrossTrackError = trackedLeg.getAverageSignedCrossTrackError(timePoint, waitForLatestAnalyses); - } catch (NoWindException nwe) { - // leave averageSignedCrossTrackError as null, meaning "unknown" - averageSignedCrossTrackError = null; - } - result.averageSignedCrossTrackErrorInMeters = averageSignedCrossTrackError == null ? null : averageSignedCrossTrackError.getMeters(); - Double speedOverGroundInKnots; - if (trackedLeg.hasFinishedLeg(timePoint)) { - speedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); - } else { - final SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); - speedOverGroundInKnots = speedOverGround == null ? null : speedOverGround.getKnots(); - } - result.currentSpeedOverGroundInKnots = speedOverGroundInKnots == null ? null : speedOverGroundInKnots; - Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint); - result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters(); - Distance distanceTraveledConsideringGateStart = trackedLeg.getDistanceTraveledConsideringGateStart(timePoint); - result.distanceTraveledIncludingGateStartInMeters = distanceTraveledConsideringGateStart == null ? null : distanceTraveledConsideringGateStart.getMeters(); - final Duration estimatedTimeToNextMarkInSeconds = trackedLeg.getEstimatedTimeToNextMark(timePoint, WindPositionMode.EXACT, cache); - result.estimatedTimeToNextWaypointInSeconds = estimatedTimeToNextMarkInSeconds==null?null:estimatedTimeToNextMarkInSeconds.asSeconds(); - result.timeInMilliseconds = time.asMillis(); - result.finished = trackedLeg.hasFinishedLeg(timePoint); - final TimePoint legFinishTime = trackedLeg.getFinishTime(); - result.correctedTotalTime = trackedLeg.hasStartedLeg(timePoint) ? trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getCorrectedTime(trackedLeg.getCompetitor(), - trackedLeg.hasFinishedLeg(timePoint) ? legFinishTime : timePoint, cache) : null; - // fetch the leg gap in own corrected time from the ranking metric - final Duration gapToLeaderInOwnTime = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric(). - getLegGapToLegLeaderInOwnTime(trackedLeg, timePoint, rankingInfo, cache); - result.gapToLeaderInSeconds = gapToLeaderInOwnTime == null ? null : gapToLeaderInOwnTime.asSeconds(); - if (result.gapToLeaderInSeconds != null) { - final Duration gapAtEndOfPreviousLeg = getGapAtEndOfPreviousLeg(trackedLeg, rankingInfo, cache); - if (gapAtEndOfPreviousLeg != null) { - result.gapChangeSinceLegStartInSeconds = result.gapToLeaderInSeconds - gapAtEndOfPreviousLeg.asSeconds(); - } - } - LinkedHashMap legRanks = legRanksCache.get(trackedLeg.getLeg()); - if (legRanks != null) { - result.rank = legRanks.get(trackedLeg.getCompetitor()); - } else { - result.rank = trackedLeg.getRank(timePoint, cache); - } - result.started = trackedLeg.hasStartedLeg(timePoint); - Speed velocityMadeGood; - if (trackedLeg.hasFinishedLeg(timePoint)) { - velocityMadeGood = trackedLeg.getAverageVelocityMadeGood(timePoint); - } else { - velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint, WindPositionMode.EXACT); - } - result.velocityMadeGoodInKnots = velocityMadeGood == null ? null : velocityMadeGood.getKnots(); - Distance windwardDistanceToGo = trackedLeg.getWindwardDistanceToGo(timePoint, WindPositionMode.LEG_MIDDLE); - result.windwardDistanceToGoInMeters = windwardDistanceToGo == null ? null : windwardDistanceToGo - .getMeters(); - final TimePoint startOfRace = trackedLeg.getTrackedLeg().getTrackedRace().getStartOfRace(); - if (startOfRace != null && trackedLeg.hasStartedLeg(timePoint)) { - // not using trackedLeg.getManeuvers(...) because it may not catch the mark passing maneuver starting this leg - // because that may have been detected as slightly before the mark passing time, hence associated with the previous leg - List maneuvers = trackedLeg.getTrackedLeg().getTrackedRace() - .getManeuvers(trackedLeg.getCompetitor(), startOfRace, timePoint, waitForLatestAnalyses); - if (maneuvers != null) { - result.numberOfManeuvers = new HashMap(); - result.numberOfManeuvers.put(ManeuverType.TACK, 0); - result.numberOfManeuvers.put(ManeuverType.JIBE, 0); - result.numberOfManeuvers.put(ManeuverType.PENALTY_CIRCLE, 0); - Map totalManeuverLossInMeters = new HashMap(); - totalManeuverLossInMeters.put(ManeuverType.TACK, 0.0); - totalManeuverLossInMeters.put(ManeuverType.JIBE, 0.0); - totalManeuverLossInMeters.put(ManeuverType.PENALTY_CIRCLE, 0.0); - TimePoint startOfLeg = trackedLeg.getStartTime(); - for (Maneuver maneuver : maneuvers) { - // don't count maneuvers that were in previous legs - switch (maneuver.getType()) { - case TACK: - case JIBE: - case PENALTY_CIRCLE: - if (!maneuver.getTimePoint().before(startOfLeg) && (legFinishTime == null || legFinishTime.after(timePoint) || - maneuver.getTimePoint().before(legFinishTime))) { - if (maneuver.getManeuverLoss() != null) { - result.numberOfManeuvers.put(maneuver.getType(), - result.numberOfManeuvers.get(maneuver.getType()) + 1); - totalManeuverLossInMeters.put(maneuver.getType(), - totalManeuverLossInMeters.get(maneuver.getType()) - + maneuver.getManeuverLoss().getMeters()); - } - } - break; - case MARK_PASSING: - // analyze all mark passings, not only those after this leg's start, to catch the mark passing - // maneuver starting this leg, even if its time point is slightly before the mark passing starting this leg - MarkPassingManeuver mpm = (MarkPassingManeuver) maneuver; - if (mpm.getWaypointPassed() == trackedLeg.getLeg().getFrom()) { - result.sideToWhichMarkAtLegStartWasRounded = mpm.getSide(); - } - break; - default: - /* Do nothing here. - * Throwing an exception destroys the toggling (and maybe other behaviour) of the leaderboard. - */ - } - } - result.averageManeuverLossInMeters = new HashMap(); - for (ManeuverType maneuverType : new ManeuverType[] { ManeuverType.TACK, ManeuverType.JIBE, - ManeuverType.PENALTY_CIRCLE }) { - if (result.numberOfManeuvers.get(maneuverType) != 0) { - result.averageManeuverLossInMeters.put( - maneuverType, - totalManeuverLossInMeters.get(maneuverType) - / result.numberOfManeuvers.get(maneuverType)); - } - } - } - } - } - return result; - } - - private Duration getGapAtEndOfPreviousLeg(TrackedLegOfCompetitor trackedLeg, final RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { - final Duration result; - final Course course = trackedLeg.getTrackedLeg().getTrackedRace().getRace().getCourse(); - // if trackedLeg is the first leg, compute the gap at the start of this leg; otherwise, compute gap - // at the end of the previous leg - final TimePoint timePoint = trackedLeg.getStartTime(); - final TrackedLegOfCompetitor tloc; - if (course.getFirstWaypoint() == trackedLeg.getLeg().getFrom()) { - tloc = trackedLeg; - } else { - tloc = trackedLeg.getTrackedLeg().getTrackedRace().getTrackedLegFinishingAt(trackedLeg.getLeg().getFrom()) - .getTrackedLeg(trackedLeg.getCompetitor()); - } - result = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getLegGapToLegLeaderInOwnTime(tloc, timePoint, rankingInfo, cache); - return result; - } - - private LeaderboardDTO getLiveLeaderboard(Collection namesOfRaceColumnsForWhichToLoadLegDetails, - boolean addOverallDetails, TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, ExecutionException { - LiveLeaderboardUpdater liveLeaderboardUpdater = getLiveLeaderboardUpdater(trackedRegattaRegistry, - baseDomainFactory); - return liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); - } - - private LiveLeaderboardUpdater getLiveLeaderboardUpdater(TrackedRegattaRegistry trackedRegattaRegistry, - DomainFactory baseDomainFactory) { - LiveLeaderboardUpdater result = this.liveLeaderboardUpdater; - if (result == null) { - synchronized (this) { - result = this.liveLeaderboardUpdater; - if (result == null) { - this.liveLeaderboardUpdater = new LiveLeaderboardUpdater(this, trackedRegattaRegistry, baseDomainFactory); - result = this.liveLeaderboardUpdater; - } - } - } - return result; - } - - private LeaderboardDTOCache getLeaderboardDTOCache() { - LeaderboardDTOCache result = this.leaderboardDTOCache; - if (result == null) { - synchronized (this) { - result = this.leaderboardDTOCache; - if (result == null) { - // The leaderboard cache is invalidated upon all competitor and mark position changes; some analyzes - // are pretty expensive, such as the maneuver re-calculation. Waiting for the latest analysis after only a - // single fix was updated is too expensive if users use the replay feature while a race is still running. - // Therefore, using waitForLatestAnalyses==false seems appropriate here. - this.leaderboardDTOCache = new LeaderboardDTOCache(/* waitForLatestAnalyses */false, this); - result = this.leaderboardDTOCache; - } - } - } - return result; - } - - @Override - public LeaderboardDTO getLeaderboardDTO(TimePoint timePoint, - Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, - TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory, boolean fillNetPointsUncorrected) throws NoWindException, - InterruptedException, ExecutionException { - LeaderboardDTO result = null; - if (timePoint == null) { - // timePoint==null means live mode; however, if we're after the end of all races and after all score - // corrections, don't use the live leaderboard updater which would keep re-calculating over and over again, but map - // this to a usual non-live call which uses the regular LeaderboardDTOCache which is invalidated properly - // when the tracked race associations or score corrections or tracked race contents changes: - final TimePoint nowMinusDelay = this.getNowMinusDelay(); - final TimePoint timePointOfLatestModification = this.getTimePointOfLatestModification(); - if (fillNetPointsUncorrected || (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification))) { - // if there hasn't been any modification to the leaderboard since nowMinusDelay, use non-live mode - // and pull the result from the regular leaderboard cache: - timePoint = timePointOfLatestModification; - } else { - // don't use the regular leaderboard cache; the race still seems to be on; use the live leaderboard updater instead: - timePoint = null; - result = this.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, trackedRegattaRegistry, baseDomainFactory); - } - } - if (timePoint != null) { - if (fillNetPointsUncorrected) { - // explicitly filling the uncorrected net points requires uncached recalculation - result = computeDTO(timePoint, namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, /* waitForLatestAnalyses */ true, - trackedRegattaRegistry, baseDomainFactory, fillNetPointsUncorrected); - } else { - // in replay we'd like up-to-date results; they are still cached - // which is OK because the cache is invalidated whenever any of the tracked races attached to the - // leaderboard changes. - result = getLeaderboardDTOCache().getLeaderboardByName(timePoint, - namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, baseDomainFactory, - trackedRegattaRegistry); - } - } - return result; - } - - public String toString() { - return getName() + " " + (getDefaultCourseArea() != null ? getDefaultCourseArea().getName() : "") + " " + (getScoringScheme() != null ? getScoringScheme().getType().name() : ""); - } - - @Override - public NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher() { - return new NumberOfCompetitorsFetcherImpl(); - } -} +package com.sap.sailing.domain.leaderboard.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +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.NavigableSet; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.sap.sailing.domain.abstractlog.race.InvalidatesLeaderboardCache; +import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.Leg; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnInSeries; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.ManeuverType; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.NoWindError; +import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaNameAndRaceName; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.dto.BasicRaceDTO; +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.FleetDTO; +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.MetaLeaderboardRaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.domain.common.dto.RaceDTO; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.NumberOfCompetitorsInLeaderboardFetcher; +import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.ScoreCorrection.Result; +import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCache; +import com.sap.sailing.domain.leaderboard.caching.LeaderboardDTOCalculationReuseCache; +import com.sap.sailing.domain.leaderboard.caching.LiveLeaderboardUpdater; +import com.sap.sailing.domain.leaderboard.meta.MetaLeaderboardColumn; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.ranking.RankingMetric.CompetitorRankingInfo; +import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.Maneuver; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.MarkPassingManeuver; +import com.sap.sailing.domain.tracking.RaceChangeListener; +import com.sap.sailing.domain.tracking.TrackedLeg; +import com.sap.sailing.domain.tracking.TrackedLegOfCompetitor; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; +import com.sap.sailing.domain.tracking.WindLegTypeAndLegBearingCache; +import com.sap.sailing.domain.tracking.WindPositionMode; +import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; +import com.sap.sailing.util.impl.RaceColumnListeners; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.concurrent.LockUtil; +import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.util.impl.ThreadFactoryWithPriority; + +/** + * Base implementation for various types of leaderboards. The {@link RaceColumnListener} implementation forwards events + * received to all {@link RaceColumnListener} subscribed with this leaderboard. To which objects this leaderboard subscribes + * as {@link RaceColumnListener} is left to the concrete subclasses to implement, but the race columns seem like useful + * candidates. + * + * @author Axel Uhl (d043530) + * + */ +public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, RaceColumnListener { + private static final long serialVersionUID = 330156778603279333L; + + private static final Logger logger = Logger.getLogger(AbstractSimpleLeaderboardImpl.class.getName()); + + static final Double DOUBLE_0 = new Double(0); + + private final SettableScoreCorrection scoreCorrection; + + private ThresholdBasedResultDiscardingRule crossLeaderboardResultDiscardingRule; + + /** + * The optional display name mappings for competitors. This allows a user to override the tracking-provided + * competitor names for display in a leaderboard. + */ + private final Map displayNames; + + /** the display name of the leaderboard */ + private String displayName; + + /** + * Backs the {@link #getCarriedPoints(Competitor)} API with data. Can be used to prime this leaderboard + * with aggregated results of races not tracked / displayed by this leaderboard in detail. The points + * provided by this map are considered by {@link #getTotalPoints(Competitor, TimePoint)}. + */ + private final Map carriedPoints; + + private final RaceColumnListeners raceColumnListeners; + + /** + * A set that manages the difference between {@link #getCompetitors()} and {@link #getAllCompetitors()}. Access + * is controlled by the {@link #suppressedCompetitorsLock} lock. + */ + private final Set suppressedCompetitors; + private final NamedReentrantReadWriteLock suppressedCompetitorsLock; + + private transient Map, RunnableFuture> raceDetailsAtEndOfTrackingCache; + + /** + * This executor needs to be a different one than {@link #executor} because the tasks run by {@link #executor} + * can depend on the results of the tasks run by {@link #raceDetailsExecutor}, and an {@link Executor} doesn't + * move a task that is blocked by waiting for another {@link FutureTask} to the side but blocks permanently, + * ending in a deadlock (one that cannot easily be detected by the Eclipse debugger either). + */ + private transient Executor raceDetailsExecutor; + + /** + * Used to remove all these listeners from their tracked races when this servlet is {@link #destroy() destroyed}. + */ + private transient Set cacheInvalidationListeners; + + private transient ThreadPoolExecutor executor; + + private transient LiveLeaderboardUpdater liveLeaderboardUpdater; + + private transient LeaderboardDTOCache leaderboardDTOCache; + + /** + * A leaderboard entry representing a snapshot of a cell at a given time point for a single race/competitor. + * + * @author Axel Uhl (d043530) + * + */ + class EntryImpl implements Entry { + private final Callable trackedRankProvider; + private final Double netPoints; + private final Callable netPointsUncorrectedProvider; + private final boolean isNetPointsCorrected; + private final Double totalPoints; + private final MaxPointsReason maxPointsReason; + private final boolean discarded; + private final Fleet fleet; + + private EntryImpl(Callable trackedRankProvider, Double netPoints, + Callable netPointsUncorrectedProvider, boolean isNetPointsCorrected, + Double totalPoints, MaxPointsReason maxPointsReason, boolean discarded, Fleet fleet) { + super(); + this.trackedRankProvider = trackedRankProvider; + this.netPoints = netPoints; + this.netPointsUncorrectedProvider = netPointsUncorrectedProvider; + this.isNetPointsCorrected = isNetPointsCorrected; + this.totalPoints = totalPoints; + this.maxPointsReason = maxPointsReason; + this.discarded = discarded; + this.fleet = fleet; + } + @Override + public int getTrackedRank() { + try { + return trackedRankProvider.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + @Override + public Double getNetPoints() { + return netPoints; + } + @Override + public boolean isNetPointsCorrected() { + return isNetPointsCorrected; + } + @Override + public Double getTotalPoints() { + return totalPoints; + } + @Override + public MaxPointsReason getMaxPointsReason() { + return maxPointsReason; + } + @Override + public boolean isDiscarded() { + return discarded; + } + @Override + public Fleet getFleet() { + return fleet; + } + @Override + public Double getNetPointsUncorrected() { + try { + return netPointsUncorrectedProvider.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + /** + * Computing the competitors can be a bit expensive, particularly if the fleet is large and there may be suppressed + * competitors, and the leaderboard may be a meta-leaderboard that refers to other leaderboards which each have + * several tracked races attached from where the competitors need to be retrieved. Ideally, the competitors list + * would be cached, but that is again difficult because we would have to monitor all changes in all dependent + * leaderboards and columns and tracked races properly. + *

    + * + * As it turns out, one of the most frequent uses of the {@link AbstractSimpleLeaderboardImpl#getCompetitors} + * competitors list is to determine their number which in turn is only required for high-point scoring systems and + * for computing the default score for penalties. Again, the most frequently used low-point family of scoring schemes + * does not require this number. Yet, the scoring scheme requires an argument for polymorphic use by those that + * need it. Instead of computing it for each call, this interface lets us defer the actual calculation until the + * point when it's really needed. Once asked, this object will cache the result. Therefore, a new one should be + * constructed each time the number shall be computed. + * + * @author Axel Uhl (D043530) + * + */ + public class NumberOfCompetitorsFetcherImpl implements NumberOfCompetitorsInLeaderboardFetcher { + private int numberOfCompetitors = -1; + + @Override + public int getNumberOfCompetitorsInLeaderboard() { + if (numberOfCompetitors == -1) { + numberOfCompetitors = Util.size(getCompetitors()); + } + return numberOfCompetitors; + } + } + + /** + * Handles the invalidation of the {@link SailingServiceImpl#raceDetailsAtEndOfTrackingCache} entries if the tracked + * race changes in any way. In particular, for {@link #statusChanged}, when the status changes away from LOADING, + * calculations may start or resume, making it necessary to clear the cache. + * + * @author Axel Uhl (D043530) + * + */ + private class CacheInvalidationListener extends AbstractRaceChangeListener { + private final TrackedRace trackedRace; + private final Competitor competitor; + + public CacheInvalidationListener(TrackedRace trackedRace, Competitor competitor) { + this.trackedRace = trackedRace; + this.competitor = competitor; + } + + public TrackedRace getTrackedRace() { + return trackedRace; + } + + public void removeFromTrackedRace() { + trackedRace.removeListener(this); + } + + private void invalidateCacheAndRemoveThisListenerFromTrackedRace() { + synchronized (raceDetailsAtEndOfTrackingCache) { + raceDetailsAtEndOfTrackingCache.remove(new com.sap.sse.common.Util.Pair(trackedRace, competitor)); + removeFromTrackedRace(); + } + } + + @Override + protected void defaultAction() { + invalidateCacheAndRemoveThisListenerFromTrackedRace(); + } + } + + private static class UUIDGenerator implements LeaderboardDTO.UUIDGenerator { + @Override + public String generateRandomUUID() { + return UUID.randomUUID().toString(); + } + } + + public AbstractSimpleLeaderboardImpl(ThresholdBasedResultDiscardingRule resultDiscardingRule) { + this.carriedPoints = new HashMap(); + this.scoreCorrection = createScoreCorrection(); + this.displayNames = new HashMap(); + this.crossLeaderboardResultDiscardingRule = resultDiscardingRule; + this.suppressedCompetitors = new HashSet(); + this.suppressedCompetitorsLock = new NamedReentrantReadWriteLock("suppressedCompetitorsLock", /* fair */ false); + this.raceColumnListeners = new RaceColumnListeners(); + this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); + initTransientFields(); + } + + /** + * Produces the score correction object to use in this leaderboard. Used by the constructor. Subclasses may override + * this method to create a more specific type of score correction. This implementation produces an object of type + * {@link ScoreCorrectionImpl}. + */ + protected SettableScoreCorrection createScoreCorrection() { + return new ScoreCorrectionImpl(this); + } + + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + initTransientFields(); + } + + private void initTransientFields() { + this.raceDetailsAtEndOfTrackingCache = new HashMap, RunnableFuture>(); + this.raceDetailsExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + this.cacheInvalidationListeners = new HashSet(); + // When many updates are triggered in a short period of time by a single thread, ensure that the single thread + // providing the updates is not outperformed by all the re-calculations happening here. Leave at least one + // core to other things, but by using at least three threads ensure that no simplistic deadlocks may occur. + final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3); + executor = new ThreadPoolExecutor(/* corePoolSize */ THREAD_POOL_SIZE, + /* maximumPoolSize */ THREAD_POOL_SIZE, + /* keepAliveTime */ 60, TimeUnit.SECONDS, + /* workQueue */ new LinkedBlockingQueue(), new ThreadFactoryWithPriority(Thread.NORM_PRIORITY-1, /* daemon */ true)); + } + + @Override + public void destroy() { + for (CacheInvalidationListener cacheInvalidationListener : cacheInvalidationListeners) { + cacheInvalidationListener.removeFromTrackedRace(); + } + } + + @Override + public SettableScoreCorrection getScoreCorrection() { + return scoreCorrection; + } + + @Override + public String getDisplayName(Competitor competitor) { + return displayNames.get(competitor); + } + + @Override + public String getDisplayName() { + return displayName; + } + + @Override + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @Override + public ResultDiscardingRule getResultDiscardingRule() { + return crossLeaderboardResultDiscardingRule; + } + + @Override + public void setCarriedPoints(Competitor competitor, double carriedPoints) { + Double oldCarriedPoints = this.carriedPoints.put(competitor, carriedPoints); + if (!Util.equalsWithNull(oldCarriedPoints, carriedPoints)) { + getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, carriedPoints); + } + } + + @Override + public double getCarriedPoints(Competitor competitor) { + Double result = carriedPoints.get(competitor); + return result == null ? 0 : result; + } + + @Override + public Map getCompetitorsForWhichThereAreCarriedPoints() { + return Collections.unmodifiableMap(carriedPoints); + } + + @Override + public void unsetCarriedPoints(Competitor competitor) { + Double oldCarriedPoints = carriedPoints.remove(competitor); + if (oldCarriedPoints != null) { + getScoreCorrection().notifyListenersAboutCarriedPointsChange(competitor, oldCarriedPoints, null); + } + } + + @Override + public boolean hasCarriedPoints() { + return !carriedPoints.isEmpty(); + } + + @Override + public boolean hasCarriedPoints(Competitor competitor) { + return carriedPoints.containsKey(competitor); + } + + @Override + public void setDisplayName(Competitor competitor, String displayName) { + String oldDisplayName = displayNames.get(competitor); + displayNames.put(competitor, displayName); + if (!Util.equalsWithNull(oldDisplayName, displayName)) { + getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); + } + } + + @Override + public void setCrossLeaderboardResultDiscardingRule(ThresholdBasedResultDiscardingRule discardingRule) { + ResultDiscardingRule oldDiscardingRule = getResultDiscardingRule(); + this.crossLeaderboardResultDiscardingRule = discardingRule; + getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, discardingRule); + } + + @Override + public Double getNetPoints(final Competitor competitor, final RaceColumn raceColumn, final TimePoint timePoint) { + return getScoreCorrection().getCorrectedScore( + new Callable() { + public Integer call() throws NoWindException { + return getTrackedRank(competitor, raceColumn, timePoint); + } + }, competitor, + raceColumn, timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()).getCorrectedScore(); + } + + @Override + public MaxPointsReason getMaxPointsReason(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + return getScoreCorrection().getMaxPointsReason(competitor, raceColumn, timePoint); + } + + @Override + public boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + return isDiscarded(competitor, raceColumn, getRaceColumns(), timePoint); + } + + private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, + Iterable raceColumnsToConsider, TimePoint timePoint) { + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + return isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns); + } + + /** + * Same as {@link #isDiscarded(Competitor, RaceColumn, TimePoint)}, only that the set of discarded race columns can + * be specified which is useful when total points are to be computed for more than one column for the same + * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done + * once and not again for each column (which would lead to quadratic effort). + * + * @param discardedRaceColumns + * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. + * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) + * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. + */ + private boolean isDiscarded(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, + final Set discardedRaceColumns) { + return !raceColumn.isMedalRace() + && getMaxPointsReason(competitor, raceColumn, timePoint).isDiscardable() + && discardedRaceColumns.contains(raceColumn); + } + + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { + return getTotalPoints(competitor, raceColumn, getRaceColumns(), timePoint); + } + + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, + Iterable raceColumnsToConsider, TimePoint timePoint) throws NoWindException { + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + return getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); + } + + /** + * Same as {@link #getTotalPoints(Competitor, RaceColumn, Iterable, TimePoint)}, only that the set of discarded race columns can + * be specified which is useful when total points are to be computed for more than one column for the same + * competitor because then the calculation of discards (which requires looking at all columns) only needs to be done + * once and not again for each column (which would lead to quadratic effort). + * + * @param discardedRaceColumns + * expected to be the result of what we would get if we called {@link #getResultDiscardingRule()}. + * {@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) + * getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint)}. + */ + @Override + public Double getTotalPoints(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint, Set discardedRaceColumns) { + Double result; + if (isDiscarded(competitor, raceColumn, timePoint, discardedRaceColumns)) { + result = 0.0; + } else { + final Double netPoints = getNetPoints(competitor, raceColumn, timePoint); + if (netPoints == null) { + result = null; + } else { + result = raceColumn.getFactor() * netPoints; + } + } + return result; + } + + @Override + public Double getTotalPoints(Competitor competitor, TimePoint timePoint) { + return getTotalPoints(competitor, getRaceColumns(), timePoint); + } + + @Override + public Double getTotalPoints(Competitor competitor, final Iterable raceColumnsToConsider, + TimePoint timePoint) { + // when a column with isStartsWithZeroScore() is found, only reset score if the competitor scored in any race from there on + boolean needToResetScoreUponNextNonEmptyEntry = false; + double result = getCarriedPoints(competitor); + final Set discardedRaceColumns = getResultDiscardingRule() + .getDiscardedRaceColumns(competitor, this, raceColumnsToConsider, timePoint); + for (RaceColumn raceColumn : raceColumnsToConsider) { + if (raceColumn.isStartsWithZeroScore()) { + needToResetScoreUponNextNonEmptyEntry = true; + } + if (getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint)) { + final Double totalPoints = getTotalPoints(competitor, raceColumn, timePoint, discardedRaceColumns); + if (totalPoints != null) { + if (needToResetScoreUponNextNonEmptyEntry) { + result = 0; + needToResetScoreUponNextNonEmptyEntry = false; + } + result += totalPoints; + } + } + } + return result; + } + + /** + * All competitors with non-null net points are added to the result which is then sorted by net points in ascending + * order. The fleet, if ordered, is the primary ordering criterion, followed by the net points. + */ + @Override + public List getCompetitorsFromBestToWorst(final RaceColumn raceColumn, TimePoint timePoint) throws NoWindException { + final Map> netPointsAndFleet = new HashMap>(); + for (Competitor competitor : getCompetitors()) { + Double netPoints = getNetPoints(competitor, raceColumn, timePoint); + if (netPoints != null) { + netPointsAndFleet.put(competitor, new com.sap.sse.common.Util.Pair(netPoints, raceColumn.getFleetOfCompetitor(competitor))); + } + } + List result = new ArrayList(netPointsAndFleet.keySet()); + Collections.sort(result, new Comparator() { + @Override + public int compare(Competitor o1, Competitor o2) { + int comparisonResult; + if (o1 == o2) { + comparisonResult = 0; + } else { + if (raceColumn.hasSplitFleets() && !raceColumn.hasSplitFleetContiguousScoring()) { + // only check fleets if there are more than one and the column is not to be contiguously scored even in case + // of split fleets + final Fleet o1Fleet = netPointsAndFleet.get(o1).getB(); + final Fleet o2Fleet = netPointsAndFleet.get(o2).getB(); + if (o1Fleet == null) { + if (o2Fleet == null) { + comparisonResult = 0; + } else { + comparisonResult = 1; // o1 ranks "worse" because it doesn't have a fleet set while o2 + // has + } + } else { + if (o2Fleet == null) { + comparisonResult = -1; // o1 ranks "better" because it has a fleet set while o2 hasn't + } else { + comparisonResult = o1Fleet.compareTo(o2Fleet); + } + } + } else { + // either there are no split fleets or the split isn't relevant for scoring as for ordered fleets + // the scoring runs contiguously from top to bottom + comparisonResult = 0; + } + if (comparisonResult == 0) { + comparisonResult = getScoringScheme().getScoreComparator(/* nullScoresAreBetter */ false).compare( + netPointsAndFleet.get(o1).getA(), netPointsAndFleet.get(o2).getA()); + } + } + return comparisonResult; + } + }); + return result; + } + + @Override + public List getCompetitorsFromBestToWorst(TimePoint timePoint) { + return getCompetitorsFromBestToWorst(getRaceColumns(), timePoint); + } + + private List getCompetitorsFromBestToWorst(Iterable raceColumnsToConsider, TimePoint timePoint) { + List result = new ArrayList(); + for (Competitor competitor : getCompetitors()) { + result.add(competitor); + } + Collections.sort(result, getTotalRankComparator(raceColumnsToConsider, timePoint)); + return result; + } + + @Override + public int getTotalRankOfCompetitor(Competitor competitor, TimePoint timePoint) throws NoWindException { + List competitorsFromBestToWorst = getCompetitorsFromBestToWorst(timePoint); + return competitorsFromBestToWorst.indexOf(competitor) + 1; + } + + protected Comparator getTotalRankComparator(Iterable raceColumnsToConsider, TimePoint timePoint) { + return new LeaderboardTotalRankComparator(this, timePoint, getScoringScheme(), /* nullScoresAreBetter */ false, raceColumnsToConsider); + } + + @Override + public RaceColumn getRaceColumnByName(String columnName) { + RaceColumn result = null; + for (RaceColumn r : getRaceColumns()) { + if (r.getName().equals(columnName)) { + result = r; + break; + } + } + return result; + } + + @Override + public Competitor getCompetitorByName(String competitorName) { + for (Competitor competitor : getAllCompetitors()) { + if (competitor.getName().equals(competitorName)) { + return competitor; + } + } + return null; + } + + @Override + public boolean countRaceForComparisonWithDiscardingThresholds(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + TrackedRace trackedRaceForCompetitorInColumn; + return getScoringScheme().isValidInTotalScore(this, raceColumn, competitor, timePoint) && + (getScoreCorrection().isScoreCorrected(competitor, raceColumn, timePoint) || + ((trackedRaceForCompetitorInColumn=raceColumn.getTrackedRace(competitor)) != null && + trackedRaceForCompetitorInColumn.hasStarted(timePoint) && + trackedRaceForCompetitorInColumn.getRank(competitor, timePoint) != 0)); + } + + @Override + public void addRaceColumnListener(RaceColumnListener listener) { + getRaceColumnListeners().addRaceColumnListener(listener); + } + + @Override + public void removeRaceColumnListener(RaceColumnListener listener) { + getRaceColumnListeners().removeRaceColumnListener(listener); + } + + @Override + public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint) throws NoWindException { + final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + return getEntry(competitor, race, timePoint, discardedRaceColumns); + } + + @Override + public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint, + Set discardedRaceColumns) throws NoWindException { + Callable trackedRankProvider = new Callable() { + public Integer call() throws NoWindException { + return getTrackedRank(competitor, race, timePoint); + } + }; + final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, race, + timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); + boolean discarded = isDiscarded(competitor, race, timePoint, discardedRaceColumns); + final Double correctedScore = correctedResults.getCorrectedScore(); + return new EntryImpl(trackedRankProvider, correctedScore, new Callable() { + @Override + public Double call() { + return correctedResults.getUncorrectedScore(); + } + }, correctedResults.isCorrected(), discarded ? DOUBLE_0 : correctedScore == null ? null + : Double.valueOf(correctedScore * race.getFactor()), correctedResults.getMaxPointsReason(), discarded, + race.getFleetOfCompetitor(competitor)); + } + + @Override + public Map> getRankedCompetitorsFromBestToWorstAfterEachRaceColumn(TimePoint timePoint) throws NoWindException { + Map> result = new LinkedHashMap<>(); + List raceColumnsToConsider = new ArrayList<>(); + for (RaceColumn raceColumn : getRaceColumns()) { + raceColumnsToConsider.add(raceColumn); + result.put(raceColumn, getCompetitorsFromBestToWorst(raceColumnsToConsider, timePoint)); + } + return result; + } + + @Override + public Map> getTotalPointsSumAfterRaceColumn(final TimePoint timePoint) + throws NoWindException { + final Map> result = new LinkedHashMap<>(); + List raceColumnsToConsider = new ArrayList<>(); + Map>> futures = new HashMap<>(); + for (final RaceColumn raceColumn : getRaceColumns()) { + raceColumnsToConsider.add(raceColumn); + final Iterable finalRaceColumnsToConsider = new ArrayList<>(raceColumnsToConsider); + futures.put(raceColumn, executor.submit(new Callable>() { + @Override + public Map call() { + Map totalPointsSumPerCompetitorInColumn = new HashMap<>(); + for (Competitor competitor : getCompetitors()) { + totalPointsSumPerCompetitorInColumn.put(competitor, getTotalPoints(competitor, finalRaceColumnsToConsider, timePoint)); + } + synchronized (result) { + return totalPointsSumPerCompetitorInColumn; + } + } + })); + } + for (RaceColumn raceColumn : getRaceColumns()) { + try { + result.put(raceColumn, futures.get(raceColumn).get()); + } catch (InterruptedException | ExecutionException e) { + if (e.getCause() instanceof NoWindError) { + throw ((NoWindError) e.getCause()).getCause(); + } else { + throw new RuntimeException(e); // no caught exceptions occur in the futures executed + } + } + } + return result; + } + + @Override + public Map, Entry> getContent(final TimePoint timePoint) throws NoWindException { + Map, Entry> result = new HashMap, Entry>(); + Map> discardedRaces = new HashMap>(); + for (final RaceColumn raceColumn : getRaceColumns()) { + for (final Competitor competitor : getCompetitors()) { + Callable trackedRankProvider = new Callable() { + @Override + public Integer call() throws Exception { + return getTrackedRank(competitor, raceColumn, timePoint); + } + }; + final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, raceColumn, + timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); + Set discardedRacesForCompetitor = discardedRaces.get(competitor); + if (discardedRacesForCompetitor == null) { + discardedRacesForCompetitor = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + discardedRaces.put(competitor, discardedRacesForCompetitor); + } + boolean discarded = discardedRacesForCompetitor.contains(raceColumn); + final Double correctedScore = correctedResults.getCorrectedScore(); + Entry entry = new EntryImpl(trackedRankProvider, correctedScore, + new Callable() { @Override public Double call() { return correctedResults.getUncorrectedScore(); } }, + correctedResults.isCorrected(), + discarded ? DOUBLE_0 : (correctedScore==null?null: + Double.valueOf((correctedScore * raceColumn.getFactor()))), correctedResults.getMaxPointsReason(), + discarded, raceColumn.getFleetOfCompetitor(competitor)); + result.put(new com.sap.sse.common.Util.Pair(competitor, raceColumn), entry); + } + } + return result; + } + + @Override + public void trackedRaceLinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); + } + + @Override + public void trackedRaceUnlinked(RaceColumn raceColumn, Fleet fleet, TrackedRace trackedRace) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); + // It's generally possible that a leaderboard links to the same tracked race in multiple columns / fleets; + // only if it no longer references the trackedRace currently unlinked from one column/fleet, also unlink + // all cache invalidation listeners for said trackedRace + if (!Util.contains(getTrackedRaces(), trackedRace)) { + synchronized (cacheInvalidationListeners) { + for (Iterator cacheInvalidationListenerIter=cacheInvalidationListeners.iterator(); + cacheInvalidationListenerIter.hasNext(); ) { + CacheInvalidationListener cacheInvalidationListener = cacheInvalidationListenerIter.next(); + if (cacheInvalidationListener.getTrackedRace() == trackedRace) { + cacheInvalidationListener.removeFromTrackedRace(); + cacheInvalidationListenerIter.remove(); + } + } + } + } + } + + @Override + public void isMedalRaceChanged(RaceColumn raceColumn, boolean newIsMedalRace) { + getRaceColumnListeners().notifyListenersAboutIsMedalRaceChanged(raceColumn, newIsMedalRace); + } + + @Override + public void isStartsWithZeroScoreChanged(RaceColumn raceColumn, boolean newIsStartsWithZeroScore) { + getRaceColumnListeners().notifyListenersAboutIsStartsWithZeroScoreChanged(raceColumn, newIsStartsWithZeroScore); + } + + @Override + public void isFirstColumnIsNonDiscardableCarryForwardChanged(RaceColumn raceColumn, boolean firstColumnIsNonDiscardableCarryForward) { + getRaceColumnListeners().notifyListenersAboutIsFirstColumnIsNonDiscardableCarryForwardChanged(raceColumn, firstColumnIsNonDiscardableCarryForward); + } + + @Override + public void hasSplitFleetContiguousScoringChanged(RaceColumn raceColumn, boolean hasSplitFleetContiguousScoring) { + getRaceColumnListeners().notifyListenersAboutHasSplitFleetContiguousScoringChanged(raceColumn, hasSplitFleetContiguousScoring); + } + + @Override + public void raceColumnMoved(RaceColumn raceColumn, int newIndex) { + getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(raceColumn, newIndex); + } + + @Override + public void factorChanged(RaceColumn raceColumn, Double oldFactor, Double newFactor) { + getRaceColumnListeners().notifyListenersAboutFactorChanged(raceColumn, oldFactor, newFactor); + } + + /** + * A leaderboard will only accept the addition of a race column if the column's name is unique across the leaderboard. + */ + @Override + public boolean canAddRaceColumnToContainer(RaceColumn newRaceColumn) { + boolean result = true; + for (RaceColumn raceColumn : getRaceColumns()) { + if (raceColumn.getName().equals(newRaceColumn.getName())) { + result = false; + break; + } + } + return result; + } + + @Override + public void raceColumnAddedToContainer(RaceColumn raceColumn) { + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(raceColumn); + } + + @Override + public void raceColumnRemovedFromContainer(RaceColumn raceColumn) { + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); + } + + @Override + public void competitorDisplayNameChanged(Competitor competitor, String oldDisplayName, String displayName) { + getRaceColumnListeners().notifyListenersAboutCompetitorDisplayNameChanged(competitor, oldDisplayName, displayName); + } + + @Override + public void resultDiscardingRuleChanged(ResultDiscardingRule oldDiscardingRule, ResultDiscardingRule newDiscardingRule) { + getRaceColumnListeners().notifyListenersAboutResultDiscardingRuleChanged(oldDiscardingRule, newDiscardingRule); + } + + @Override + public boolean isTransient() { + return false; + } + + /** + * Finds out the time point when any of the {@link Leaderboard#getTrackedRaces() tracked races currently attached to + * the leaderboard} and the {@link Leaderboard#getScoreCorrection() score corrections} have last been + * modified. If no tracked race is attached and no time-stamped score corrections have been applied to the leaderboard, + * null is returned. The time point computed this way is a good choice for normalizing queries for later time + * points in an attempt to achieve more cache hits.

    + * + * Note, however, that the result does not tell about structural changes to the leaderboard and therefore cannot be used + * to determine the need for cache invalidation. For example, if a column is added to a leaderboard after the time point + * returned by this method but that column's attached tracked race has finished before the time point returned by this method, + * the result of this method won't change. Still, the contents of the leaderboard will change by a change in column structure. + * A different means to determine the possibility of changes that happened to this leaderboard must be used for cache + * management. Such a facility has to listen for score correction changes, tracked races being attached or detached and + * the column structure changing. + * + * @see TrackedRace#getTimePointOfNewestEvent() + * @see SettableScoreCorrection#getTimePointOfLastCorrectionsValidity() + */ + @Override + public TimePoint getTimePointOfLatestModification() { + TimePoint result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + if (result == null || (trackedRace.getTimePointOfNewestEvent() != null && trackedRace.getTimePointOfNewestEvent().after(result))) { + result = trackedRace.getTimePointOfNewestEvent(); + } + } + TimePoint timePointOfLastScoreCorrection = getScoreCorrection().getTimePointOfLastCorrectionsValidity(); + if (timePointOfLastScoreCorrection != null && (result == null || timePointOfLastScoreCorrection.after(result))) { + result = timePointOfLastScoreCorrection; + } + return result; + } + + @Override + public com.sap.sse.common.Util.Pair getMaximumSpeedOverGround(Competitor competitor, TimePoint timePoint) { + com.sap.sse.common.Util.Pair result = null; + // TODO should we ensure that competitor participated in all race columns? + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + NavigableSet markPassings = trackedRace.getMarkPassings(competitor); + if (!markPassings.isEmpty()) { + TimePoint from = markPassings.first().getTimePoint(); + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) && + markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + to = timePoint; + } + com.sap.sse.common.Util.Pair maxSpeed = trackedRace.getTrack(competitor).getMaximumSpeedOverGround(from, to); + if (result == null || result.getB() == null || + (maxSpeed != null && maxSpeed.getB() != null && maxSpeed.getB().compareTo(result.getB()) > 0)) { + result = maxSpeed; + } + } + } + } + return result; + } + + @Override + public Speed getAverageSpeedOverGround(Competitor competitor, TimePoint timePoint) { + Speed result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + NavigableSet markPassings = trackedRace.getMarkPassings(competitor); + if (!markPassings.isEmpty()) { + TimePoint from = markPassings.first().getTimePoint(); + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) && + markPassings.last().getWaypoint() == trackedRace.getRace().getCourse().getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + if (markPassings.last().getWaypoint() != trackedRace.getRace().getCourse().getLastWaypoint() && + timePoint.after(markPassings.last().getTimePoint())) { + result = null; + break; + } + to = timePoint; + } + Distance distanceTraveled = trackedRace.getDistanceTraveled(competitor, timePoint); + if (distanceTraveled != null) { + result = distanceTraveled.inTime(to.asMillis()-from.asMillis()); + } + } + } + } + return result; + } + + @Override + public Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint) throws NoWindException { + return getTotalTimeSailedInLegType(competitor, legType, timePoint, new HashMap()); + } + + private Duration getTotalTimeSailedInLegType(Competitor competitor, LegType legType, TimePoint timePoint, Map legTypeCache) throws NoWindException { + Duration result = null; + // TODO should we ensure that competitor participated in all race columns? + outerLoop: + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + trackedRace.getRace().getCourse().lockForRead(); + try { + for (Leg leg : trackedRace.getRace().getCourse().getLegs()) { + TrackedLegOfCompetitor trackedLegOfCompetitor = trackedRace.getTrackedLeg(competitor, leg); + if (trackedLegOfCompetitor.hasStartedLeg(timePoint)) { + // find out leg type at the time the competitor started the leg + try { + final TrackedLeg trackedLeg = trackedRace.getTrackedLeg(leg); + LegType trackedLegType = legTypeCache.get(trackedLeg); + if (trackedLegType == null) { + final TimePoint startTime = trackedLegOfCompetitor.getStartTime(); + TimePoint finishTime = trackedLegOfCompetitor.getFinishTime(); + if (finishTime == null) { + finishTime = timePoint; + } + trackedLegType = trackedLeg.getLegType(startTime.plus(startTime.until(finishTime).divide(2))); // middle of the leg + legTypeCache.put(trackedLeg, trackedLegType); + } + if (legType == trackedLegType) { + Duration timeSpentOnDownwind = trackedLegOfCompetitor.getTime(timePoint); + if (timeSpentOnDownwind != null) { + if (result == null) { + result = timeSpentOnDownwind; + } else { + result = result.plus(timeSpentOnDownwind); + } + } else { + // Although the competitor has started the leg, no value was produced. This + // means that the competitor didn't finish the leg before tracking ended. No useful value + // can be obtained for this competitor anymore. + result = null; + break outerLoop; + } + } + } catch (NoWindException nwe) { + // without wind there is no leg type and hence there is no reasonable value for this: + result = null; + break outerLoop; + } + } + } + } finally { + trackedRace.getRace().getCourse().unlockAfterRead(); + } + } + } + return result; + } + + @Override + public Duration getTotalTimeSailed(Competitor competitor, TimePoint timePoint) { + Duration result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { + NavigableSet markPassings = trackedRace.getMarkPassings(competitor); + if (!markPassings.isEmpty()) { + TimePoint from = trackedRace.getStartOfRace(); // start counting at race start, not when the competitor passed the line + if (from != null && !timePoint.before(from)) { // but only if the race started after timePoint + TimePoint to; + if (timePoint.after(markPassings.last().getTimePoint()) + && markPassings.last().getWaypoint() == trackedRace.getRace().getCourse() + .getLastWaypoint()) { + // stop counting when competitor finished the race + to = markPassings.last().getTimePoint(); + } else { + if (trackedRace.getEndOfTracking() != null + && timePoint.after(trackedRace.getEndOfTracking())) { + result = null; // race not finished until end of tracking; no reasonable value can be + // computed for competitor + break; + } else { + to = timePoint; + } + } + Duration timeSpent = from.until(to); + if (result == null) { + result = timeSpent; + } else { + result=result.plus(timeSpent); + } + } + } + } + } + return result; + } + + @Override + public Distance getTotalDistanceTraveled(Competitor competitor, TimePoint timePoint) { + Distance result = null; + for (TrackedRace trackedRace : getTrackedRaces()) { + TimePoint startOfRace; + if (Util.contains(trackedRace.getRace().getCompetitors(), competitor) && + (startOfRace=trackedRace.getStartOfRace()) != null && + !startOfRace.after(timePoint)) { + Distance distanceSailedInRace = trackedRace.getDistanceTraveled(competitor, timePoint); + if (distanceSailedInRace != null) { + if (result == null) { + result = distanceSailedInRace; + } else { + result = result.add(distanceSailedInRace); + } + } else { + // if competitor has not finished one single race in the whole + // series then we can not return a meaningful value for all + // all races + return null; + } + } + } + return result; + } + + protected RaceColumnListeners getRaceColumnListeners() { + return raceColumnListeners; + } + + @Override + public Iterable getCompetitors() { + final Iterable result; + // mostly the set of suppressed competitors is empty; in this case, avoid having to loop over the + // potentially large set of competitors + if (Util.isEmpty(getSuppressedCompetitors())) { + result = getAllCompetitors(); + } else { + final Iterable allCompetitors = getAllCompetitors(); + final Set suppressed = new HashSet<>(); + Util.addAll(getSuppressedCompetitors(), suppressed); + result = getCompetitorIterableSkippingSuppressed(allCompetitors, suppressed); + } + return result; + } + + @Override + public Iterable getCompetitors(RaceColumn raceColumn, Fleet fleet) { + return getCompetitorIterableSkippingSuppressed(getAllCompetitors(raceColumn, fleet), getSuppressedCompetitors()); + } + + /** + * return an iterable with a smart iterator that filters out the suppressed elements on demand + */ + protected Iterable getCompetitorIterableSkippingSuppressed(final Iterable allCompetitors, + final Iterable suppressed) { + final Iterable result; + result = new Iterable() { + @Override + public Iterator iterator() { + return new Iterator() { + private final Iterator allIter = allCompetitors.iterator(); + private Competitor next = advance(); + + private Competitor advance() { + next = null; + while (allIter.hasNext() && next == null) { + next = allIter.next(); + if (Util.contains(suppressed, next)) { + next = null; + } + } + return next; + } + + @Override + public boolean hasNext() { + return next != null; + } + + @Override + public Competitor next() { + if (next == null) { + throw new NoSuchElementException(); + } + final Competitor result = next; + advance(); + return result; + } + }; + } + }; + return result; + } + + @Override + public Iterable getSuppressedCompetitors() { + LockUtil.lockForRead(suppressedCompetitorsLock); + try { + return new HashSet(suppressedCompetitors); + } finally { + LockUtil.unlockAfterRead(suppressedCompetitorsLock); + } + } + + @Override + public void setSuppressed(Competitor competitor, boolean suppressed) { + LockUtil.lockForWrite(suppressedCompetitorsLock); + try { + if (suppressed) { + suppressedCompetitors.add(competitor); + } else { + suppressedCompetitors.remove(competitor); + } + } finally { + LockUtil.unlockAfterWrite(suppressedCompetitorsLock); + } + getScoreCorrection().notifyListenersAboutIsSuppressedChange(competitor, suppressed); + } + + @Override + public TimePoint getNowMinusDelay() { + final TimePoint now = MillisecondsTimePoint.now(); + final Long delayToLiveInMillis = getDelayToLiveInMillis(); + TimePoint timePoint = delayToLiveInMillis == null ? now : now.minus(delayToLiveInMillis); + return timePoint; + } + + @Override + public void raceLogEventAdded(RaceColumn raceColumn, RaceLogIdentifier raceLogIdentifier, RaceLogEvent event) { + getRaceColumnListeners().notifyListenersAboutRaceLogEventAdded(raceColumn, raceLogIdentifier, event); + if (event instanceof InvalidatesLeaderboardCache) { + // make sure to invalidate the cache as this event indicates that + // it changes values the cache could still hold + if (leaderboardDTOCache != null) { + leaderboardDTOCache.invalidate(this); + } + } + } + + @Override + public LeaderboardDTO computeDTO(final TimePoint timePoint, + final Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + final boolean waitForLatestAnalyses, TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory, + final boolean fillNetPointsUncorrected) + throws NoWindException { + long startOfRequestHandling = System.currentTimeMillis(); + final LeaderboardDTOCalculationReuseCache cache = new LeaderboardDTOCalculationReuseCache(timePoint); + final LeaderboardDTO result = new LeaderboardDTO(this.getScoreCorrection().getTimePointOfLastCorrectionsValidity() == null ? null + : this.getScoreCorrection().getTimePointOfLastCorrectionsValidity().asDate(), + this.getScoreCorrection() == null ? null : this.getScoreCorrection().getComment(), + this.getScoringScheme() == null ? null : this.getScoringScheme().getType(), this + .getScoringScheme().isHigherBetter(), new UUIDGenerator(), addOverallDetails); + result.type = getLeaderboardType(); + result.competitors = new ArrayList(); + result.name = this.getName(); + result.competitorDisplayNames = new HashMap(); + for (Competitor suppressedCompetitor : this.getSuppressedCompetitors()) { + result.setSuppressed(baseDomainFactory.convertToCompetitorDTO(suppressedCompetitor), true); + } + // Now create the race columns and, as a future task, set their competitorsFromBestToWorst, then wait for all these + // futures to finish: + Map>> competitorsFromBestToWorstTasks = new HashMap<>(); + for (final RaceColumn raceColumn : this.getRaceColumns()) { + boolean isMetaLeaderboardColumn = raceColumn instanceof MetaLeaderboardColumn; + RaceColumnDTO raceColumnDTO = result.createEmptyRaceColumn(raceColumn.getName(), raceColumn.isMedalRace(), + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, + isMetaLeaderboardColumn); + if (isMetaLeaderboardColumn && raceColumnDTO instanceof MetaLeaderboardRaceColumnDTO) { + calculateRacesMetadata((MetaLeaderboardColumn) raceColumn, (MetaLeaderboardRaceColumnDTO) raceColumnDTO, + trackedRegattaRegistry, baseDomainFactory); + } + for (Fleet fleet : raceColumn.getFleets()) { + RegattaAndRaceIdentifier raceIdentifier = null; + RaceDTO race = null; + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + final FleetDTO fleetDTO = baseDomainFactory.convertToFleetDTO(fleet); + if (trackedRace != null) { + raceIdentifier = new RegattaNameAndRaceName(trackedRace.getTrackedRegatta().getRegatta().getName(), + trackedRace.getRace().getName()); + race = baseDomainFactory.createRaceDTO(trackedRegattaRegistry, /* withGeoLocationData */ false, raceIdentifier, trackedRace); + } + // Note: the RaceColumnDTO won't be created by the following addRace call because it has been created + // above by the result.createEmptyRaceColumn call + result.addRace(raceColumn.getName(), raceColumn.getExplicitFactor(), raceColumn.getFactor(), + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getRegatta().getName() : null, + raceColumn instanceof RaceColumnInSeries ? ((RaceColumnInSeries) raceColumn).getSeries().getName() : null, + fleetDTO, raceColumn.isMedalRace(), raceIdentifier, race, isMetaLeaderboardColumn); + } + FutureTask> task = new FutureTask>( + () -> baseDomainFactory.getCompetitorDTOList(AbstractSimpleLeaderboardImpl.this.getCompetitorsFromBestToWorst(raceColumn, timePoint))); + executor.execute(task); + competitorsFromBestToWorstTasks.put(raceColumn, task); + } + // wait for the competitor orderings to have been computed for all race columns before continuing; subsequent tasks may depend on these data + for (Map.Entry>> raceColumnAndTaskToJoin : competitorsFromBestToWorstTasks.entrySet()) { + try { + result.setCompetitorsFromBestToWorst(raceColumnAndTaskToJoin.getKey().getName(), raceColumnAndTaskToJoin.getValue().get()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept + // null values. + // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So + // this helps robustness from a user's perspective. + logger.log( + Level.SEVERE, + AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " + + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails + + "): exception during computing competitor ordering for race column "+raceColumnAndTaskToJoin.getKey().getName(), e); + } + } + result.setDelayToLiveInMillisForLatestRace(this.getDelayToLiveInMillis()); + result.rows = new HashMap(); + result.hasCarriedPoints = this.hasCarriedPoints(); + if (this.getResultDiscardingRule() instanceof ThresholdBasedResultDiscardingRule) { + result.discardThresholds = ((ThresholdBasedResultDiscardingRule) this.getResultDiscardingRule()) + .getDiscardIndexResultsStartingWithHowManyRaces(); + } else { + result.discardThresholds = null; + } + // Computing the competitor leg ranks is expensive, especially in live mode, in case new events keep + // invalidating the ranks cache in TrackedLegImpl. The problem then is that the sorting based on wind data is repeated for + // each competitor, leading to square effort. We therefore need to compute the leg ranks for those races where leg + // details are requested only once and pass them into getLeaderboardEntryDTO + final Map> legRanksCache = new HashMap>(); + for (final RaceColumn raceColumn : this.getRaceColumns()) { + // if details for the column are requested, cache the leg's ranks + if (namesOfRaceColumnsForWhichToLoadLegDetails != null + && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn.getName())) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + trackedRace.getRace().getCourse().lockForRead(); + try { + for (TrackedLeg trackedLeg : trackedRace.getTrackedLegs()) { + legRanksCache.put(trackedLeg.getLeg(), trackedLeg.getRanks(timePoint, cache)); + } + } finally { + trackedRace.getRace().getCourse().unlockAfterRead(); + } + } + } + } + } + for (final Competitor competitor : this.getCompetitorsFromBestToWorst(timePoint)) { + CompetitorDTO competitorDTO = baseDomainFactory.convertToCompetitorDTO(competitor); + LeaderboardRowDTO row = new LeaderboardRowDTO(); + row.competitor = competitorDTO; + row.fieldsByRaceColumnName = new HashMap(); + row.carriedPoints = this.hasCarriedPoints(competitor) ? this.getCarriedPoints(competitor) : null; + row.totalPoints = this.getTotalPoints(competitor, timePoint); + if (addOverallDetails) { + addOverallDetailsToRow(timePoint, competitor, row); + } + result.competitors.add(competitorDTO); + Map> futuresForColumnName = new HashMap>(); + final Set discardedRaceColumns = getResultDiscardingRule().getDiscardedRaceColumns(competitor, this, getRaceColumns(), timePoint); + for (final RaceColumn raceColumn : this.getRaceColumns()) { + RunnableFuture future = new FutureTask(() -> { + Entry entry = AbstractSimpleLeaderboardImpl.this.getEntry(competitor, raceColumn, timePoint, discardedRaceColumns); + return getLeaderboardEntryDTO(entry, raceColumn, competitor, timePoint, + namesOfRaceColumnsForWhichToLoadLegDetails != null + && namesOfRaceColumnsForWhichToLoadLegDetails.contains(raceColumn + .getName()), waitForLatestAnalyses, legRanksCache, baseDomainFactory, + fillNetPointsUncorrected, cache); + }); + executor.execute(future); + futuresForColumnName.put(raceColumn.getName(), future); + } + for (Map.Entry> raceColumnNameAndFuture : futuresForColumnName.entrySet()) { + try { + row.fieldsByRaceColumnName.put(raceColumnNameAndFuture.getKey(), raceColumnNameAndFuture.getValue().get()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + // See also bug 1371: for stability reasons, don't let the exception percolate but rather accept + // null values. + // If new evidence is provided, a re-calculation of the leaderboard will be triggered anyway. So + // this helps robustness from a user's perspective. + logger.log( + Level.SEVERE, + AbstractSimpleLeaderboardImpl.class.getName() + ".computeDTO(" + this.getName() + ", " + + timePoint + ", " + namesOfRaceColumnsForWhichToLoadLegDetails+", addOverallDetails="+addOverallDetails + + "): exception during computing leaderboard entry for competitor " + + competitor.getName() + " in race column " + raceColumnNameAndFuture.getKey() + + ". Leaving empty.", e); + } + } + result.rows.put(competitorDTO, row); + String displayName = this.getDisplayName(competitor); + if (displayName != null) { + result.competitorDisplayNames.put(competitorDTO, displayName); + } + } + logger.info("computeLeaderboardByName(" + this.getName() + ", " + timePoint + ", " + + namesOfRaceColumnsForWhichToLoadLegDetails + ", addOverallDetails=" + addOverallDetails + ") took " + + (System.currentTimeMillis() - startOfRequestHandling) + "ms"); + return result; + } + + private void calculateRacesMetadata(MetaLeaderboardColumn metaLeaderboardColumn, MetaLeaderboardRaceColumnDTO columnDTO, + TrackedRegattaRegistry trackedRegattaRegistry, final DomainFactory baseDomainFactory) { + for (final RaceColumn raceColumn : metaLeaderboardColumn.getLeaderboard().getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + String regattaName = trackedRace.getTrackedRegatta().getRegatta().getName(); + String raceName = trackedRace.getRace().getName(); + RegattaAndRaceIdentifier raceIdentifier = new RegattaNameAndRaceName(regattaName, raceName); + columnDTO.addRace(new BasicRaceDTO(raceIdentifier, baseDomainFactory.createTrackedRaceDTO(trackedRace))); + } + } + } + } + + private void addOverallDetailsToRow(final TimePoint timePoint, + final Competitor competitor, LeaderboardRowDTO row) throws NoWindException { + final com.sap.sse.common.Util.Pair maximumSpeedOverGround = this.getMaximumSpeedOverGround(competitor, timePoint); + if (maximumSpeedOverGround != null && maximumSpeedOverGround.getB() != null) { + row.maximumSpeedOverGroundInKnots = maximumSpeedOverGround.getB().getKnots(); + row.whenMaximumSpeedOverGroundWasAchieved = maximumSpeedOverGround.getA().getTimePoint().asDate(); + } + Map legTypeCache = new HashMap<>(); + final Duration totalTimeSailedDownwind = this.getTotalTimeSailedInLegType(competitor, LegType.DOWNWIND, timePoint, legTypeCache); + row.totalTimeSailedDownwindInSeconds = totalTimeSailedDownwind==null?null:totalTimeSailedDownwind.asSeconds(); + final Duration totalTimeSailedUpwind = this.getTotalTimeSailedInLegType(competitor, LegType.UPWIND, timePoint, legTypeCache); + row.totalTimeSailedUpwindInSeconds = totalTimeSailedUpwind==null?null:totalTimeSailedUpwind.asSeconds(); + final Duration totalTimeSailedReaching = this.getTotalTimeSailedInLegType(competitor, LegType.REACHING, timePoint, legTypeCache); + row.totalTimeSailedReachingInSeconds = totalTimeSailedReaching==null?null:totalTimeSailedReaching.asSeconds(); + final Duration totalTimeSailed = this.getTotalTimeSailed(competitor, timePoint); + row.totalTimeSailedInSeconds = totalTimeSailed==null?null:totalTimeSailed.asSeconds(); + final Distance totalDistanceTraveledInMeters = this.getTotalDistanceTraveled(competitor, timePoint); + row.totalDistanceTraveledInMeters = totalDistanceTraveledInMeters==null?null:totalDistanceTraveledInMeters.getMeters(); + } + + /** + * @param waitForLatestAnalyses + * if false, this method is allowed to read the maneuver analysis results from a cache that + * may not reflect all data already received; otherwise, the method will always block for the latest + * cache updates to have happened before returning. + * @param fillNetPointsUncorrected + * tells if {@link LeaderboardEntryDTO#netPointsUncorrected} shall be filled; filling it is rather + * expensive, especially when compared to simply retrieving a score correction, and particularly if in a + * larger fleet a number of competitors haven't properly finished the race. This should only be used for + * leaderboard editing where a user needs to see what the uncorrected score was that would be used when + * the correction was removed. + */ + private LeaderboardEntryDTO getLeaderboardEntryDTO(Entry entry, RaceColumn raceColumn, Competitor competitor, + TimePoint timePoint, boolean addLegDetails, boolean waitForLatestAnalyses, + Map> legRanksCache, DomainFactory baseDomainFactory, + boolean fillNetPointsUncorrected, WindLegTypeAndLegBearingCache cache) { + LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); + TrackedRace trackedRace = raceColumn.getTrackedRace(competitor); + entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); + entryDTO.netPoints = entry.getNetPoints(); + if (fillNetPointsUncorrected) { + entryDTO.netPointsUncorrected = entry.getNetPointsUncorrected(); + } + entryDTO.netPointsCorrected = entry.isNetPointsCorrected(); + entryDTO.totalPoints = entry.getTotalPoints(); + entryDTO.reasonForMaxPoints = entry.getMaxPointsReason(); + entryDTO.discarded = entry.isDiscarded(); + final GPSFixTrack track = trackedRace == null ? null : trackedRace.getTrack(competitor); + if (trackedRace != null) { + Date timePointOfLastPositionFixAtOrBeforeQueryTimePoint = getTimePointOfLastFixAtOrBefore(competitor, trackedRace, timePoint); + if (track != null) { + entryDTO.averageSamplingInterval = track.getAverageIntervalBetweenRawFixes(); + } + if (timePointOfLastPositionFixAtOrBeforeQueryTimePoint != null) { + long timeDifferenceInMs = timePoint.asMillis() - timePointOfLastPositionFixAtOrBeforeQueryTimePoint.getTime(); + entryDTO.timeSinceLastPositionFixInSeconds = timeDifferenceInMs == 0 ? 0.0 : timeDifferenceInMs / 1000.0; + } else { + entryDTO.timeSinceLastPositionFixInSeconds = null; + } + } + if (addLegDetails && trackedRace != null) { + try { + final RankingInfo rankingInfo = trackedRace.getRankingMetric().getRankingInfo(timePoint, cache); + RaceDetails raceDetails = getRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, + legRanksCache, rankingInfo, cache); + entryDTO.legDetails = raceDetails.getLegDetails(); + entryDTO.windwardDistanceToCompetitorFarthestAheadInMeters = raceDetails.getWindwardDistanceToCompetitorFarthestAhead() == null ? null + : raceDetails.getWindwardDistanceToCompetitorFarthestAhead().getMeters(); + entryDTO.gapToLeaderInOwnTime = trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache); + entryDTO.averageAbsoluteCrossTrackErrorInMeters = raceDetails.getAverageAbsoluteCrossTrackError() == null ? null + : raceDetails.getAverageAbsoluteCrossTrackError().getMeters(); + entryDTO.averageSignedCrossTrackErrorInMeters = raceDetails.getAverageSignedCrossTrackError() == null ? null + : raceDetails.getAverageSignedCrossTrackError().getMeters(); + entryDTO.calculatedTime = raceDetails.getCorrectedTime(); + entryDTO.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = raceDetails.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead(); + entryDTO.gapToLeaderInOwnTime = raceDetails.getGapToLeaderInOwnTime(); + final TimePoint startOfRace = trackedRace.getStartOfRace(); + if (startOfRace != null) { + Waypoint startWaypoint = trackedRace.getRace().getCourse().getFirstWaypoint(); + NavigableSet competitorMarkPassings = trackedRace.getMarkPassings(competitor); + trackedRace.lockForRead(competitorMarkPassings); + try { + if (!competitorMarkPassings.isEmpty()) { + final MarkPassing firstMarkPassing = competitorMarkPassings.iterator().next(); + if (firstMarkPassing.getWaypoint() == startWaypoint) { + Distance distanceToStartLineFiveSecondsBeforeStartOfRace = trackedRace.getDistanceToStartLine(competitor, /*milliseconds before start*/ 5000); + entryDTO.distanceToStartLineFiveSecondsBeforeStartInMeters = distanceToStartLineFiveSecondsBeforeStartOfRace == null ? null + : distanceToStartLineFiveSecondsBeforeStartOfRace.getMeters(); + Speed speedFiveSecondsBeforeStartOfRace = trackedRace.getSpeed(competitor, /*milliseconds before start*/ 5000); + entryDTO.speedOverGroundFiveSecondsBeforeStartInKnots = speedFiveSecondsBeforeStartOfRace == null ? null + : speedFiveSecondsBeforeStartOfRace.getKnots(); + Distance distanceToStartLineAtStartOfRace = trackedRace.getDistanceToStartLine( + competitor, startOfRace); + entryDTO.distanceToStartLineAtStartOfRaceInMeters = distanceToStartLineAtStartOfRace == null ? null + : distanceToStartLineAtStartOfRace.getMeters(); + Speed speedAtStartTime = track == null ? null : track.getEstimatedSpeed(startOfRace); + entryDTO.speedOverGroundAtStartOfRaceInKnots = speedAtStartTime == null ? null + : speedAtStartTime.getKnots(); + TimePoint competitorStartTime = firstMarkPassing.getTimePoint(); + entryDTO.timeBetweenRaceStartAndCompetitorStartInSeconds = startOfRace.until(competitorStartTime).asSeconds(); + Speed competitorSpeedWhenPassingStart = track == null ? null : track + .getEstimatedSpeed(competitorStartTime); + entryDTO.speedOverGroundAtPassingStartWaypointInKnots = competitorSpeedWhenPassingStart == null ? null + : competitorSpeedWhenPassingStart.getKnots(); + try { + entryDTO.startTack = trackedRace.getTack(competitor, competitorStartTime); + } catch (NoWindException nwe) { + entryDTO.startTack = null; // leave empty in case no wind information is available + } + Distance distanceFromStarboardSideOfStartLineWhenPassingStart = trackedRace + .getDistanceFromStarboardSideOfStartLineWhenPassingStart(competitor); + entryDTO.distanceToStarboardSideOfStartLineInMeters = distanceFromStarboardSideOfStartLineWhenPassingStart == null ? null + : distanceFromStarboardSideOfStartLineWhenPassingStart.getMeters(); + } + } + } finally { + trackedRace.unlockAfterRead(competitorMarkPassings); + } + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e); // the future used to calculate the leg details was interrupted; escalate as runtime exception + } + } + final Fleet fleet = entry.getFleet(); + entryDTO.fleet = fleet == null ? null : baseDomainFactory.convertToFleetDTO(fleet); + return entryDTO; + } + + /** + * Determines the time point of the last raw fix (with outliers not removed) for competitor in + * trackedRace. If the competitor's track is null or empty, null is returned. + * @param trackedRace must not be null + * @param atOrBefore find the last fix at or before the time point specified + */ + private Date getTimePointOfLastFixAtOrBefore(Competitor competitor, TrackedRace trackedRace, TimePoint atOrBefore) { + assert trackedRace != null; + final Date timePointOfLastPositionFix; + GPSFixTrack track = trackedRace.getTrack(competitor); + if (track == null) { + timePointOfLastPositionFix = null; + } else { + GPSFixMoving lastFix = track.getLastFixAtOrBefore(atOrBefore); + if (lastFix == null) { + timePointOfLastPositionFix = null; + } else { + timePointOfLastPositionFix = lastFix.getTimePoint().asDate(); + } + } + return timePointOfLastPositionFix; + } + + private static class RaceDetails { + private final List legDetails; + private final Distance windwardDistanceToCompetitorFarthestAhead; + private final Distance averageAbsoluteCrossTrackError; + private final Distance averageSignedCrossTrackError; + private final Duration gapToLeaderInOwnTime; + private final Duration correctedTime; + private final Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + + public RaceDetails(List legDetails, Distance windwardDistanceToCompetitorFarthestAhead, + Distance averageAbsoluteCrossTrackError, Distance averageSignedCrossTrackError, + Duration gapToLeaderInOwnTime, Duration correctedTime, + Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { + super(); + this.legDetails = legDetails; + this.windwardDistanceToCompetitorFarthestAhead = windwardDistanceToCompetitorFarthestAhead; + this.averageAbsoluteCrossTrackError = averageAbsoluteCrossTrackError; + this.averageSignedCrossTrackError = averageSignedCrossTrackError; + this.gapToLeaderInOwnTime = gapToLeaderInOwnTime; + this.correctedTime = correctedTime; + this.correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + } + public List getLegDetails() { + return legDetails; + } + public Distance getWindwardDistanceToCompetitorFarthestAhead() { + return windwardDistanceToCompetitorFarthestAhead; + } + public Distance getAverageAbsoluteCrossTrackError() { + return averageAbsoluteCrossTrackError; + } + public Distance getAverageSignedCrossTrackError() { + return averageSignedCrossTrackError; + } + public Duration getGapToLeaderInOwnTime() { + return gapToLeaderInOwnTime; + } + public Duration getCorrectedTime() { + return correctedTime; + } + public Duration getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead() { + return correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; + } + } + + /** + * If timePoint is after the end of the race's tracking the query will be adjusted to obtain the values + * at the end of the {@link TrackedRace#getEndOfTracking() race's tracking time}. If the time point adjusted this + * way equals the end of the tracking time, the query results will be looked up in a cache first and if not found, + * they will be stored to the cache after calculating them. A cache invalidation {@link RaceChangeListener listener} + * will be registered with the race which will be triggered for any event received by the race. + * @param waitForLatestAnalyses + * if false, this method is allowed to read the maneuver analysis results from a cache that + * may not reflect all data already received; otherwise, the method will always block for the latest + * cache updates to have happened before returning. + */ + private RaceDetails getRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { + final RaceDetails raceDetails; + if (trackedRace.getEndOfTracking() != null && trackedRace.getEndOfTracking().compareTo(timePoint) < 0) { + raceDetails = getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(trackedRace, competitor, legRanksCache, rankingInfo, cache); + } else { + raceDetails = calculateRaceDetails(trackedRace, competitor, timePoint, waitForLatestAnalyses, legRanksCache, cache, rankingInfo); + } + return raceDetails; + } + + private RaceDetails getRaceDetailsForEndOfTrackingFromCacheOrCalculateAndCache(final TrackedRace trackedRace, + final Competitor competitor, final Map> legRanksCache, + RankingInfo rankingInfo, final WindLegTypeAndLegBearingCache cache) throws InterruptedException, ExecutionException { + final com.sap.sse.common.Util.Pair key = new com.sap.sse.common.Util.Pair(trackedRace, competitor); + RunnableFuture raceDetails; + synchronized (raceDetailsAtEndOfTrackingCache) { + raceDetails = raceDetailsAtEndOfTrackingCache.get(key); + if (raceDetails == null) { + raceDetails = new FutureTask(new Callable() { + @Override + public RaceDetails call() throws Exception { + TimePoint end = trackedRace.getEndOfRace(); + if (end == null) { + end = trackedRace.getEndOfTracking(); + } + return calculateRaceDetails(trackedRace, competitor, end, + // TODO see bug 1358: for now, use waitForLatest==false until we've switched to optimistic locking for the course read lock + /* TODO old comment when it was still true: "because this is done only once after end of tracking" */ + /* waitForLatestAnalyses (maneuver and cross track error) */ false, + legRanksCache, cache, rankingInfo); + } + }); + raceDetailsExecutor.execute(raceDetails); + raceDetailsAtEndOfTrackingCache.put(key, raceDetails); + final CacheInvalidationListener cacheInvalidationListener = new CacheInvalidationListener(trackedRace, competitor); + trackedRace.addListener(cacheInvalidationListener); + cacheInvalidationListeners.add(cacheInvalidationListener); + } + } + return raceDetails.get(); + } + + private RaceDetails calculateRaceDetails(TrackedRace trackedRace, Competitor competitor, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + WindLegTypeAndLegBearingCache cache, final RankingInfo rankingInfo) { + final List legDetails = new ArrayList(); + final Course course = trackedRace.getRace().getCourse(); + course.lockForRead(); // hold back any course re-configurations while looping over the legs + try { + for (Leg leg : course.getLegs()) { + LegEntryDTO legEntry; + // We loop over a copy of the course's legs; during a course change, legs may become "stale," even with + // regard to the leg/trackedLeg structures inside the tracked race which is updated by the course change + // immediately. That's why we've acquired a read lock for the course above. + TrackedLegOfCompetitor trackedLeg = trackedRace.getTrackedLeg(competitor, leg); + if (trackedLeg != null && trackedLeg.hasStartedLeg(timePoint)) { + legEntry = createLegEntry(trackedLeg, timePoint, waitForLatestAnalyses, legRanksCache, rankingInfo, cache); + } else { + legEntry = null; + } + legDetails.add(legEntry); + } + final Distance windwardDistanceToCompetitorFarthestAhead = trackedRace == null ? null : trackedRace + .getWindwardDistanceToCompetitorFarthestAhead(competitor, timePoint, WindPositionMode.LEG_MIDDLE, rankingInfo, cache); + Distance averageAbsoluteCrossTrackError; + try { + averageAbsoluteCrossTrackError = trackedRace == null ? null : trackedRace.getAverageAbsoluteCrossTrackError( + competitor, timePoint, waitForLatestAnalyses, cache); + } catch (NoWindException nwe) { + // without wind information, use null meaning "unknown" + averageAbsoluteCrossTrackError = null; + } + Distance averageSignedCrossTrackError; + try { + averageSignedCrossTrackError = trackedRace == null ? null : trackedRace.getAverageSignedCrossTrackError( + competitor, timePoint, waitForLatestAnalyses, cache); + } catch (NoWindException nwe) { + // without wind information, use null meaning "unknown" + averageSignedCrossTrackError = null; + } + final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); + return new RaceDetails(legDetails, windwardDistanceToCompetitorFarthestAhead, averageAbsoluteCrossTrackError, averageSignedCrossTrackError, + trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache), + trackedRace.getRankingMetric().getCorrectedTime(competitor, timePoint), + competitorRankingInfo == null ? null : competitorRankingInfo.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead()); + } finally { + course.unlockAfterRead(); + } + } + + private LegEntryDTO createLegEntry(TrackedLegOfCompetitor trackedLeg, TimePoint timePoint, + boolean waitForLatestAnalyses, Map> legRanksCache, + RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { + LegEntryDTO result; + final Duration time = trackedLeg.getTime(timePoint); + if (trackedLeg == null || time == null) { + result = null; + } else { + result = new LegEntryDTO(); + try { + result.legType = trackedLeg.getTrackedLeg().getLegType(timePoint); + } catch (NoWindException nwe) { + result.legType = null; // can't determine leg type without wind data + } + final Speed averageSpeedOverGround = trackedLeg.getAverageSpeedOverGround(timePoint); + result.averageSpeedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); + Distance averageAbsoluteCrossTrackError; + try { + averageAbsoluteCrossTrackError = trackedLeg.getAverageAbsoluteCrossTrackError(timePoint, waitForLatestAnalyses); + } catch (NoWindException nwe) { + // leave averageAbsoluteCrossTrackError as null, meaning "unknown" + averageAbsoluteCrossTrackError = null; + } + result.averageAbsoluteCrossTrackErrorInMeters = averageAbsoluteCrossTrackError == null ? null : averageAbsoluteCrossTrackError.getMeters(); + Distance averageSignedCrossTrackError; + try { + averageSignedCrossTrackError = trackedLeg.getAverageSignedCrossTrackError(timePoint, waitForLatestAnalyses); + } catch (NoWindException nwe) { + // leave averageSignedCrossTrackError as null, meaning "unknown" + averageSignedCrossTrackError = null; + } + result.averageSignedCrossTrackErrorInMeters = averageSignedCrossTrackError == null ? null : averageSignedCrossTrackError.getMeters(); + Double speedOverGroundInKnots; + if (trackedLeg.hasFinishedLeg(timePoint)) { + speedOverGroundInKnots = averageSpeedOverGround == null ? null : averageSpeedOverGround.getKnots(); + } else { + final SpeedWithBearing speedOverGround = trackedLeg.getSpeedOverGround(timePoint); + speedOverGroundInKnots = speedOverGround == null ? null : speedOverGround.getKnots(); + } + result.currentSpeedOverGroundInKnots = speedOverGroundInKnots == null ? null : speedOverGroundInKnots; + Distance distanceTraveled = trackedLeg.getDistanceTraveled(timePoint); + result.distanceTraveledInMeters = distanceTraveled == null ? null : distanceTraveled.getMeters(); + Distance distanceTraveledConsideringGateStart = trackedLeg.getDistanceTraveledConsideringGateStart(timePoint); + result.distanceTraveledIncludingGateStartInMeters = distanceTraveledConsideringGateStart == null ? null : distanceTraveledConsideringGateStart.getMeters(); + final Duration estimatedTimeToNextMarkInSeconds = trackedLeg.getEstimatedTimeToNextMark(timePoint, WindPositionMode.EXACT, cache); + result.estimatedTimeToNextWaypointInSeconds = estimatedTimeToNextMarkInSeconds==null?null:estimatedTimeToNextMarkInSeconds.asSeconds(); + result.timeInMilliseconds = time.asMillis(); + result.finished = trackedLeg.hasFinishedLeg(timePoint); + final TimePoint legFinishTime = trackedLeg.getFinishTime(); + result.correctedTotalTime = trackedLeg.hasStartedLeg(timePoint) ? trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getCorrectedTime(trackedLeg.getCompetitor(), + trackedLeg.hasFinishedLeg(timePoint) ? legFinishTime : timePoint, cache) : null; + // fetch the leg gap in own corrected time from the ranking metric + final Duration gapToLeaderInOwnTime = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric(). + getLegGapToLegLeaderInOwnTime(trackedLeg, timePoint, rankingInfo, cache); + result.gapToLeaderInSeconds = gapToLeaderInOwnTime == null ? null : gapToLeaderInOwnTime.asSeconds(); + if (result.gapToLeaderInSeconds != null) { + final Duration gapAtEndOfPreviousLeg = getGapAtEndOfPreviousLeg(trackedLeg, rankingInfo, cache); + if (gapAtEndOfPreviousLeg != null) { + result.gapChangeSinceLegStartInSeconds = result.gapToLeaderInSeconds - gapAtEndOfPreviousLeg.asSeconds(); + } + } + LinkedHashMap legRanks = legRanksCache.get(trackedLeg.getLeg()); + if (legRanks != null) { + result.rank = legRanks.get(trackedLeg.getCompetitor()); + } else { + result.rank = trackedLeg.getRank(timePoint, cache); + } + result.started = trackedLeg.hasStartedLeg(timePoint); + Speed velocityMadeGood; + if (trackedLeg.hasFinishedLeg(timePoint)) { + velocityMadeGood = trackedLeg.getAverageVelocityMadeGood(timePoint); + } else { + velocityMadeGood = trackedLeg.getVelocityMadeGood(timePoint, WindPositionMode.EXACT); + } + result.velocityMadeGoodInKnots = velocityMadeGood == null ? null : velocityMadeGood.getKnots(); + Distance windwardDistanceToGo = trackedLeg.getWindwardDistanceToGo(timePoint, WindPositionMode.LEG_MIDDLE); + result.windwardDistanceToGoInMeters = windwardDistanceToGo == null ? null : windwardDistanceToGo + .getMeters(); + final TimePoint startOfRace = trackedLeg.getTrackedLeg().getTrackedRace().getStartOfRace(); + if (startOfRace != null && trackedLeg.hasStartedLeg(timePoint)) { + // not using trackedLeg.getManeuvers(...) because it may not catch the mark passing maneuver starting this leg + // because that may have been detected as slightly before the mark passing time, hence associated with the previous leg + List maneuvers = trackedLeg.getTrackedLeg().getTrackedRace() + .getManeuvers(trackedLeg.getCompetitor(), startOfRace, timePoint, waitForLatestAnalyses); + if (maneuvers != null) { + result.numberOfManeuvers = new HashMap(); + result.numberOfManeuvers.put(ManeuverType.TACK, 0); + result.numberOfManeuvers.put(ManeuverType.JIBE, 0); + result.numberOfManeuvers.put(ManeuverType.PENALTY_CIRCLE, 0); + Map totalManeuverLossInMeters = new HashMap(); + totalManeuverLossInMeters.put(ManeuverType.TACK, 0.0); + totalManeuverLossInMeters.put(ManeuverType.JIBE, 0.0); + totalManeuverLossInMeters.put(ManeuverType.PENALTY_CIRCLE, 0.0); + TimePoint startOfLeg = trackedLeg.getStartTime(); + for (Maneuver maneuver : maneuvers) { + // don't count maneuvers that were in previous legs + switch (maneuver.getType()) { + case TACK: + case JIBE: + case PENALTY_CIRCLE: + if (!maneuver.getTimePoint().before(startOfLeg) && (legFinishTime == null || legFinishTime.after(timePoint) || + maneuver.getTimePoint().before(legFinishTime))) { + if (maneuver.getManeuverLoss() != null) { + result.numberOfManeuvers.put(maneuver.getType(), + result.numberOfManeuvers.get(maneuver.getType()) + 1); + totalManeuverLossInMeters.put(maneuver.getType(), + totalManeuverLossInMeters.get(maneuver.getType()) + + maneuver.getManeuverLoss().getMeters()); + } + } + break; + case MARK_PASSING: + // analyze all mark passings, not only those after this leg's start, to catch the mark passing + // maneuver starting this leg, even if its time point is slightly before the mark passing starting this leg + MarkPassingManeuver mpm = (MarkPassingManeuver) maneuver; + if (mpm.getWaypointPassed() == trackedLeg.getLeg().getFrom()) { + result.sideToWhichMarkAtLegStartWasRounded = mpm.getSide(); + } + break; + default: + /* Do nothing here. + * Throwing an exception destroys the toggling (and maybe other behaviour) of the leaderboard. + */ + } + } + result.averageManeuverLossInMeters = new HashMap(); + for (ManeuverType maneuverType : new ManeuverType[] { ManeuverType.TACK, ManeuverType.JIBE, + ManeuverType.PENALTY_CIRCLE }) { + if (result.numberOfManeuvers.get(maneuverType) != 0) { + result.averageManeuverLossInMeters.put( + maneuverType, + totalManeuverLossInMeters.get(maneuverType) + / result.numberOfManeuvers.get(maneuverType)); + } + } + } + } + } + return result; + } + + private Duration getGapAtEndOfPreviousLeg(TrackedLegOfCompetitor trackedLeg, final RankingInfo rankingInfo, WindLegTypeAndLegBearingCache cache) { + final Duration result; + final Course course = trackedLeg.getTrackedLeg().getTrackedRace().getRace().getCourse(); + // if trackedLeg is the first leg, compute the gap at the start of this leg; otherwise, compute gap + // at the end of the previous leg + final TimePoint timePoint = trackedLeg.getStartTime(); + final TrackedLegOfCompetitor tloc; + if (course.getFirstWaypoint() == trackedLeg.getLeg().getFrom()) { + tloc = trackedLeg; + } else { + tloc = trackedLeg.getTrackedLeg().getTrackedRace().getTrackedLegFinishingAt(trackedLeg.getLeg().getFrom()) + .getTrackedLeg(trackedLeg.getCompetitor()); + } + result = trackedLeg.getTrackedLeg().getTrackedRace().getRankingMetric().getLegGapToLegLeaderInOwnTime(tloc, timePoint, rankingInfo, cache); + return result; + } + + private LeaderboardDTO getLiveLeaderboard(Collection namesOfRaceColumnsForWhichToLoadLegDetails, + boolean addOverallDetails, TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory) throws NoWindException, ExecutionException { + LiveLeaderboardUpdater liveLeaderboardUpdater = getLiveLeaderboardUpdater(trackedRegattaRegistry, + baseDomainFactory); + return liveLeaderboardUpdater.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails); + } + + private LiveLeaderboardUpdater getLiveLeaderboardUpdater(TrackedRegattaRegistry trackedRegattaRegistry, + DomainFactory baseDomainFactory) { + LiveLeaderboardUpdater result = this.liveLeaderboardUpdater; + if (result == null) { + synchronized (this) { + result = this.liveLeaderboardUpdater; + if (result == null) { + this.liveLeaderboardUpdater = new LiveLeaderboardUpdater(this, trackedRegattaRegistry, baseDomainFactory); + result = this.liveLeaderboardUpdater; + } + } + } + return result; + } + + private LeaderboardDTOCache getLeaderboardDTOCache() { + LeaderboardDTOCache result = this.leaderboardDTOCache; + if (result == null) { + synchronized (this) { + result = this.leaderboardDTOCache; + if (result == null) { + // The leaderboard cache is invalidated upon all competitor and mark position changes; some analyzes + // are pretty expensive, such as the maneuver re-calculation. Waiting for the latest analysis after only a + // single fix was updated is too expensive if users use the replay feature while a race is still running. + // Therefore, using waitForLatestAnalyses==false seems appropriate here. + this.leaderboardDTOCache = new LeaderboardDTOCache(/* waitForLatestAnalyses */false, this); + result = this.leaderboardDTOCache; + } + } + } + return result; + } + + @Override + public LeaderboardDTO getLeaderboardDTO(TimePoint timePoint, + Collection namesOfRaceColumnsForWhichToLoadLegDetails, boolean addOverallDetails, + TrackedRegattaRegistry trackedRegattaRegistry, DomainFactory baseDomainFactory, boolean fillNetPointsUncorrected) throws NoWindException, + InterruptedException, ExecutionException { + LeaderboardDTO result = null; + if (timePoint == null) { + // timePoint==null means live mode; however, if we're after the end of all races and after all score + // corrections, don't use the live leaderboard updater which would keep re-calculating over and over again, but map + // this to a usual non-live call which uses the regular LeaderboardDTOCache which is invalidated properly + // when the tracked race associations or score corrections or tracked race contents changes: + final TimePoint nowMinusDelay = this.getNowMinusDelay(); + final TimePoint timePointOfLatestModification = this.getTimePointOfLatestModification(); + if (fillNetPointsUncorrected || (timePointOfLatestModification != null && !nowMinusDelay.before(timePointOfLatestModification))) { + // if there hasn't been any modification to the leaderboard since nowMinusDelay, use non-live mode + // and pull the result from the regular leaderboard cache: + timePoint = timePointOfLatestModification; + } else { + // don't use the regular leaderboard cache; the race still seems to be on; use the live leaderboard updater instead: + timePoint = null; + result = this.getLiveLeaderboard(namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, trackedRegattaRegistry, baseDomainFactory); + } + } + if (timePoint != null) { + if (fillNetPointsUncorrected) { + // explicitly filling the uncorrected net points requires uncached recalculation + result = computeDTO(timePoint, namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, /* waitForLatestAnalyses */ true, + trackedRegattaRegistry, baseDomainFactory, fillNetPointsUncorrected); + } else { + // in replay we'd like up-to-date results; they are still cached + // which is OK because the cache is invalidated whenever any of the tracked races attached to the + // leaderboard changes. + result = getLeaderboardDTOCache().getLeaderboardByName(timePoint, + namesOfRaceColumnsForWhichToLoadLegDetails, addOverallDetails, baseDomainFactory, + trackedRegattaRegistry); + } + } + return result; + } + + public String toString() { + return getName() + " " + (getDefaultCourseArea() != null ? getDefaultCourseArea().getName() : "") + " " + (getScoringScheme() != null ? getScoringScheme().getType().name() : ""); + } + + @Override + public NumberOfCompetitorsInLeaderboardFetcher getNumberOfCompetitorsInLeaderboardFetcher() { + return new NumberOfCompetitorsFetcherImpl(); + } + + protected abstract LeaderboardType getLeaderboardType(); + +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java index ab37c03d57d..62d50860e6a 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/FlexibleLeaderboardImpl.java @@ -1,336 +1,342 @@ -package com.sap.sailing.domain.leaderboard.impl; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.ObjectStreamException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Logger; - -import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.base.impl.AbstractRaceExecutionOrderProvider; -import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; -import com.sap.sailing.domain.leaderboard.FlexibleRaceColumn; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.racelog.RaceLogStore; -import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; -import com.sap.sailing.domain.regattalike.BaseRegattaLikeImpl; -import com.sap.sailing.domain.regattalike.FlexibleLeaderboardAsRegattaLikeIdentifier; -import com.sap.sailing.domain.regattalike.IsRegattaLike; -import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; -import com.sap.sailing.domain.regattalike.RegattaLikeListener; -import com.sap.sailing.domain.regattalog.RegattaLogStore; -import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore; -import com.sap.sailing.domain.tracking.RaceExecutionOrderProvider; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sse.common.Duration; - -/** - * A leaderboard implementation that allows users to flexibly configure which columns exist. No constraints need to be observed regarding - * the columns belonging to the same regatta or even boat class.

    - * - * The flexible leaderboard listens as {@link RaceColumnListener} on all its {@link RaceColumn}s and forwards all events to - * all {@link RaceColumnListener}s subscribed with this leaderboard. - * - * @author Axel Uhl (D043530) - * - */ -public class FlexibleLeaderboardImpl extends AbstractLeaderboardImpl implements FlexibleLeaderboard { - private static Logger logger = Logger.getLogger(FlexibleLeaderboardImpl.class.getName()); - - protected static final DefaultFleetImpl defaultFleet = new DefaultFleetImpl(); - private static final long serialVersionUID = -5708971849158747846L; - private final List races; - private final ScoringScheme scoringScheme; - private String name; - private transient RaceLogStore raceLogStore; - private CourseArea courseArea; - private RaceExecutionOrderProvider raceExecutionOrderProvider; - - /** - * @see RegattaLog for the reason why the leaderboard manages a {@code RegattaLog} - */ - private final IsRegattaLike regattaLikeHelper; - - public FlexibleLeaderboardImpl(String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, - ScoringScheme scoringScheme, CourseArea courseArea) { - this(EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, - name, resultDiscardingRule, scoringScheme, courseArea); - } - - public FlexibleLeaderboardImpl(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, - String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, - ScoringScheme scoringScheme, CourseArea courseArea) { - super(resultDiscardingRule); - this.scoringScheme = scoringScheme; - if (name == null) { - throw new IllegalArgumentException("A leaderboard's name must not be null"); - } - this.name = name; - this.races = new ArrayList<>(); - this.raceLogStore = raceLogStore; - this.courseArea = courseArea; - this.regattaLikeHelper = new BaseRegattaLikeImpl(new FlexibleLeaderboardAsRegattaLikeIdentifier(this), regattaLogStore) { - private static final long serialVersionUID = 4082392360832548953L; - - @Override - public RaceColumn getRaceColumnByName(String raceColumnName) { - return getRaceColumnByName(raceColumnName); - } - }; - this.raceExecutionOrderProvider = new RaceExecutionOrderCache(); - } - - /** - * Deserialization has to be maintained in lock-step with {@link #writeObject(ObjectOutputStream) serialization}. - * When de-serializing, a possibly remote {@link #raceLogStore} is ignored because it is transient. Instead, an - * {@link EmptyRaceLogStore} is used for the de-serialized instance. A new {@link RaceLogInformation} is - * assembled for this empty race log and applied to all columns. - */ - private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { - ois.defaultReadObject(); - raceLogStore = EmptyRaceLogStore.INSTANCE; - for (RaceColumn column : getRaceColumns()) { - column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); - } - } - - protected Object readResolve() throws ObjectStreamException { - raceExecutionOrderProvider.triggerUpdate(); - return this; - } - - @Override - public String getName() { - return name; - } - - /** - * @param newName must not be null - */ - public void setName(String newName) { - if (newName == null) { - throw new IllegalArgumentException("A leaderboard's name must not be null"); - } - this.name = newName; - } - - @Override - public RaceColumn addRace(TrackedRace race, String columnName, boolean medalRace) { - FlexibleRaceColumn column = addRaceColumn(columnName, medalRace, /* logAlreadyExistingColumn */false); - column.setTrackedRace(defaultFleet, race); // triggers listeners because this object was registered above as - // race column listener on the column - return column; - } - - @Override - public FlexibleRaceColumn addRaceColumn(String name, boolean medalRace) { - return addRaceColumn(name, medalRace, /* logAlreadyExistingColumn */true); - } - - private FlexibleRaceColumn addRaceColumn(String name, boolean medalRace, boolean logAlreadyExistingColumn) { - FlexibleRaceColumn column = getRaceColumnByName(name); - if (column != null) { - if (logAlreadyExistingColumn) { - final String msg = "Trying to create race column with duplicate name " + name + " in leaderboard " + getName(); - logger.severe(msg); - } - } else { - column = createRaceColumn(name, medalRace); - column.addRaceColumnListener(this); - races.add(column); - column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(column); - } - return column; - } - - @Override - public FlexibleRaceColumn getRaceColumnByName(String columnName) { - return (FlexibleRaceColumn) super.getRaceColumnByName(columnName); - } - - @Override - public Fleet getFleet(String fleetName) { - Fleet result; - if (fleetName == null) { - result = defaultFleet; - } else { - result = super.getFleet(fleetName); - } - return result; - } - - @Override - public void removeRaceColumn(String columnName) { - final FlexibleRaceColumn raceColumn = getRaceColumnByName(columnName); - if (raceColumn != null) { - for (Fleet fleet : raceColumn.getFleets()) { - raceLogStore.removeRaceLog(raceColumn.getRaceLogIdentifier(fleet)); - if (raceColumn.getTrackedRace(fleet) != null) { - raceColumn.getTrackedRace(fleet).detachRaceExecutionOrderProvider(raceExecutionOrderProvider); - } - } - races.remove(raceColumn); - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); - raceColumn.removeRaceColumnListener(this); - } - } - - @Override - public Iterable getRaceColumns() { - final Iterable result; - if (races != null) { - result = Collections.unmodifiableCollection(new ArrayList(races)); - } else { - result = null; - } - return result; - } - - protected RaceColumnImpl createRaceColumn(String column, boolean medalRace) { - return new RaceColumnImpl(column, medalRace, raceExecutionOrderProvider); - } - - protected Iterable turnNullOrEmptyFleetsIntoDefaultFleet(Fleet... fleets) { - Iterable theFleets; - if (fleets == null || fleets.length == 0) { - Fleet defaultfleetCasted = defaultFleet; - theFleets = Collections.singleton(defaultfleetCasted); - } else { - theFleets = Arrays.asList(fleets); - } - return theFleets; - } - - @Override - public void moveRaceColumnUp(String name) { - FlexibleRaceColumn race = null; - for (FlexibleRaceColumn r : races) { - if (r.getName().equals(name)) { - race = r; - } - } - if (race != null) { - int index = 0; - index = races.lastIndexOf(race); - index--; - if (index >= 0) { - races.remove(race); - races.add(index, race); - getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); - } - } - } - - @Override - public void moveRaceColumnDown(String name) { - FlexibleRaceColumn race = null; - for (FlexibleRaceColumn r : races) { - if (r.getName().equals(name)) { - race = r; - } - } - if (race != null) { - int index = 0; - index = races.lastIndexOf(race); - if (index != -1) { - index++; - if (index < races.size()) { - races.remove(race); - races.add(index, race); - getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); - } - } - } - } - - @Override - public void updateIsMedalRace(String raceName, boolean isMedalRace) { - FlexibleRaceColumn race = null; - for (FlexibleRaceColumn r : races) { - if (r.getName().equals(raceName)) - race = r; - } - if (race != null) { - race.setIsMedalRace(isMedalRace); - } - } - - @Override - public ScoringScheme getScoringScheme() { - return scoringScheme; - } - - @Override - public CourseArea getDefaultCourseArea() { - return courseArea; - } - - @Override - public void setDefaultCourseArea(CourseArea newCourseArea) { - this.courseArea = newCourseArea; - } - - @Override - public IsRegattaLike getRegattaLike() { - return regattaLikeHelper; - } - - @Override - public RegattaLog getRegattaLog() { - return regattaLikeHelper.getRegattaLog(); - } - - @Override - public RegattaLikeIdentifier getRegattaLikeIdentifier() { - return regattaLikeHelper.getRegattaLikeIdentifier(); - } - - @Override - public void addListener(RegattaLikeListener listener) { - regattaLikeHelper.addListener(listener); - } - - @Override - public void removeListener(RegattaLikeListener listener) { - regattaLikeHelper.removeListener(listener); - } - - @Override - public Double getTimeOnTimeFactor(Competitor competitor) { - return regattaLikeHelper.getTimeOnTimeFactor(competitor); - } - - @Override - public Duration getTimeOnDistanceAllowancePerNauticalMile(Competitor competitor) { - return regattaLikeHelper.getTimeOnDistanceAllowancePerNauticalMile(competitor); - } - - private class RaceExecutionOrderCache extends AbstractRaceExecutionOrderProvider { - private static final long serialVersionUID = 652833386555762661L; - - public RaceExecutionOrderCache() { - super(); - addRaceColumnListener(this); - } - - @Override - protected Map> getRaceColumnsOfSeries() { - final Map> result = new HashMap<>(); - result.put(FlexibleLeaderboardImpl.defaultFleet, getRaceColumns()); - return result; - } - } -} +package com.sap.sailing.domain.leaderboard.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.ObjectStreamException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.impl.AbstractRaceExecutionOrderProvider; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.FlexibleRaceColumn; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.impl.EmptyRaceLogStore; +import com.sap.sailing.domain.regattalike.BaseRegattaLikeImpl; +import com.sap.sailing.domain.regattalike.FlexibleLeaderboardAsRegattaLikeIdentifier; +import com.sap.sailing.domain.regattalike.IsRegattaLike; +import com.sap.sailing.domain.regattalike.RegattaLikeIdentifier; +import com.sap.sailing.domain.regattalike.RegattaLikeListener; +import com.sap.sailing.domain.regattalog.RegattaLogStore; +import com.sap.sailing.domain.regattalog.impl.EmptyRegattaLogStore; +import com.sap.sailing.domain.tracking.RaceExecutionOrderProvider; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.Duration; + +/** + * A leaderboard implementation that allows users to flexibly configure which columns exist. No constraints need to be observed regarding + * the columns belonging to the same regatta or even boat class.

    + * + * The flexible leaderboard listens as {@link RaceColumnListener} on all its {@link RaceColumn}s and forwards all events to + * all {@link RaceColumnListener}s subscribed with this leaderboard. + * + * @author Axel Uhl (D043530) + * + */ +public class FlexibleLeaderboardImpl extends AbstractLeaderboardImpl implements FlexibleLeaderboard { + private static Logger logger = Logger.getLogger(FlexibleLeaderboardImpl.class.getName()); + + protected static final DefaultFleetImpl defaultFleet = new DefaultFleetImpl(); + private static final long serialVersionUID = -5708971849158747846L; + private final List races; + private final ScoringScheme scoringScheme; + private String name; + private transient RaceLogStore raceLogStore; + private CourseArea courseArea; + private RaceExecutionOrderProvider raceExecutionOrderProvider; + + /** + * @see RegattaLog for the reason why the leaderboard manages a {@code RegattaLog} + */ + private final IsRegattaLike regattaLikeHelper; + + public FlexibleLeaderboardImpl(String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, + ScoringScheme scoringScheme, CourseArea courseArea) { + this(EmptyRaceLogStore.INSTANCE, EmptyRegattaLogStore.INSTANCE, + name, resultDiscardingRule, scoringScheme, courseArea); + } + + public FlexibleLeaderboardImpl(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, + String name, ThresholdBasedResultDiscardingRule resultDiscardingRule, + ScoringScheme scoringScheme, CourseArea courseArea) { + super(resultDiscardingRule); + this.scoringScheme = scoringScheme; + if (name == null) { + throw new IllegalArgumentException("A leaderboard's name must not be null"); + } + this.name = name; + this.races = new ArrayList<>(); + this.raceLogStore = raceLogStore; + this.courseArea = courseArea; + this.regattaLikeHelper = new BaseRegattaLikeImpl(new FlexibleLeaderboardAsRegattaLikeIdentifier(this), regattaLogStore) { + private static final long serialVersionUID = 4082392360832548953L; + + @Override + public RaceColumn getRaceColumnByName(String raceColumnName) { + return getRaceColumnByName(raceColumnName); + } + }; + this.raceExecutionOrderProvider = new RaceExecutionOrderCache(); + } + + /** + * Deserialization has to be maintained in lock-step with {@link #writeObject(ObjectOutputStream) serialization}. + * When de-serializing, a possibly remote {@link #raceLogStore} is ignored because it is transient. Instead, an + * {@link EmptyRaceLogStore} is used for the de-serialized instance. A new {@link RaceLogInformation} is + * assembled for this empty race log and applied to all columns. + */ + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + raceLogStore = EmptyRaceLogStore.INSTANCE; + for (RaceColumn column : getRaceColumns()) { + column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); + } + } + + protected Object readResolve() throws ObjectStreamException { + raceExecutionOrderProvider.triggerUpdate(); + return this; + } + + @Override + public String getName() { + return name; + } + + /** + * @param newName must not be null + */ + public void setName(String newName) { + if (newName == null) { + throw new IllegalArgumentException("A leaderboard's name must not be null"); + } + this.name = newName; + } + + @Override + public RaceColumn addRace(TrackedRace race, String columnName, boolean medalRace) { + FlexibleRaceColumn column = addRaceColumn(columnName, medalRace, /* logAlreadyExistingColumn */false); + column.setTrackedRace(defaultFleet, race); // triggers listeners because this object was registered above as + // race column listener on the column + return column; + } + + @Override + public FlexibleRaceColumn addRaceColumn(String name, boolean medalRace) { + return addRaceColumn(name, medalRace, /* logAlreadyExistingColumn */true); + } + + private FlexibleRaceColumn addRaceColumn(String name, boolean medalRace, boolean logAlreadyExistingColumn) { + FlexibleRaceColumn column = getRaceColumnByName(name); + if (column != null) { + if (logAlreadyExistingColumn) { + final String msg = "Trying to create race column with duplicate name " + name + " in leaderboard " + getName(); + logger.severe(msg); + } + } else { + column = createRaceColumn(name, medalRace); + column.addRaceColumnListener(this); + races.add(column); + column.setRaceLogInformation(raceLogStore, new FlexibleLeaderboardAsRegattaLikeIdentifier(this)); + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(column); + } + return column; + } + + @Override + public FlexibleRaceColumn getRaceColumnByName(String columnName) { + return (FlexibleRaceColumn) super.getRaceColumnByName(columnName); + } + + @Override + public Fleet getFleet(String fleetName) { + Fleet result; + if (fleetName == null) { + result = defaultFleet; + } else { + result = super.getFleet(fleetName); + } + return result; + } + + @Override + public void removeRaceColumn(String columnName) { + final FlexibleRaceColumn raceColumn = getRaceColumnByName(columnName); + if (raceColumn != null) { + for (Fleet fleet : raceColumn.getFleets()) { + raceLogStore.removeRaceLog(raceColumn.getRaceLogIdentifier(fleet)); + if (raceColumn.getTrackedRace(fleet) != null) { + raceColumn.getTrackedRace(fleet).detachRaceExecutionOrderProvider(raceExecutionOrderProvider); + } + } + races.remove(raceColumn); + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(raceColumn); + raceColumn.removeRaceColumnListener(this); + } + } + + @Override + public Iterable getRaceColumns() { + final Iterable result; + if (races != null) { + result = Collections.unmodifiableCollection(new ArrayList(races)); + } else { + result = null; + } + return result; + } + + protected RaceColumnImpl createRaceColumn(String column, boolean medalRace) { + return new RaceColumnImpl(column, medalRace, raceExecutionOrderProvider); + } + + protected Iterable turnNullOrEmptyFleetsIntoDefaultFleet(Fleet... fleets) { + Iterable theFleets; + if (fleets == null || fleets.length == 0) { + Fleet defaultfleetCasted = defaultFleet; + theFleets = Collections.singleton(defaultfleetCasted); + } else { + theFleets = Arrays.asList(fleets); + } + return theFleets; + } + + @Override + public void moveRaceColumnUp(String name) { + FlexibleRaceColumn race = null; + for (FlexibleRaceColumn r : races) { + if (r.getName().equals(name)) { + race = r; + } + } + if (race != null) { + int index = 0; + index = races.lastIndexOf(race); + index--; + if (index >= 0) { + races.remove(race); + races.add(index, race); + getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); + } + } + } + + @Override + public void moveRaceColumnDown(String name) { + FlexibleRaceColumn race = null; + for (FlexibleRaceColumn r : races) { + if (r.getName().equals(name)) { + race = r; + } + } + if (race != null) { + int index = 0; + index = races.lastIndexOf(race); + if (index != -1) { + index++; + if (index < races.size()) { + races.remove(race); + races.add(index, race); + getRaceColumnListeners().notifyListenersAboutRaceColumnMoved(race, index); + } + } + } + } + + @Override + public void updateIsMedalRace(String raceName, boolean isMedalRace) { + FlexibleRaceColumn race = null; + for (FlexibleRaceColumn r : races) { + if (r.getName().equals(raceName)) + race = r; + } + if (race != null) { + race.setIsMedalRace(isMedalRace); + } + } + + @Override + public ScoringScheme getScoringScheme() { + return scoringScheme; + } + + @Override + public CourseArea getDefaultCourseArea() { + return courseArea; + } + + @Override + public void setDefaultCourseArea(CourseArea newCourseArea) { + this.courseArea = newCourseArea; + } + + @Override + public IsRegattaLike getRegattaLike() { + return regattaLikeHelper; + } + + @Override + public RegattaLog getRegattaLog() { + return regattaLikeHelper.getRegattaLog(); + } + + @Override + public RegattaLikeIdentifier getRegattaLikeIdentifier() { + return regattaLikeHelper.getRegattaLikeIdentifier(); + } + + @Override + public void addListener(RegattaLikeListener listener) { + regattaLikeHelper.addListener(listener); + } + + @Override + public void removeListener(RegattaLikeListener listener) { + regattaLikeHelper.removeListener(listener); + } + + @Override + public Double getTimeOnTimeFactor(Competitor competitor) { + return regattaLikeHelper.getTimeOnTimeFactor(competitor); + } + + @Override + public Duration getTimeOnDistanceAllowancePerNauticalMile(Competitor competitor) { + return regattaLikeHelper.getTimeOnDistanceAllowancePerNauticalMile(competitor); + } + + private class RaceExecutionOrderCache extends AbstractRaceExecutionOrderProvider { + private static final long serialVersionUID = 652833386555762661L; + + public RaceExecutionOrderCache() { + super(); + addRaceColumnListener(this); + } + + @Override + protected Map> getRaceColumnsOfSeries() { + final Map> result = new HashMap<>(); + result.put(FlexibleLeaderboardImpl.defaultFleet, getRaceColumns()); + return result; + } + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.FlexibleLeaderboard; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java index a6df854049f..d5af239cf6a 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/RegattaLeaderboardImpl.java @@ -1,117 +1,123 @@ -package com.sap.sailing.domain.leaderboard.impl; - -import java.util.ArrayList; -import java.util.List; - -import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnInSeries; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.base.Regatta; -import com.sap.sailing.domain.base.Series; -import com.sap.sailing.domain.base.impl.RaceColumnInSeriesImpl; -import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; -import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.regattalike.IsRegattaLike; -import com.sap.sailing.domain.tracking.TrackedRace; - -/** - * A leaderboard that is based on the definition of a {@link Regatta} with its {@link Series} and {@link Fleet}. The regatta - * leaderboard listens to its {@link Regatta} as a {@link RaceColumnListener} and forwards all link/unlink events for - * {@link TrackedRace}s being linked to / unlinked from race columns to all {@link RaceColumnListener}s subscribed with - * this leaderboard. - * - * @author Axel Uhl (D043530) - * - */ -public class RegattaLeaderboardImpl extends AbstractLeaderboardImpl implements RegattaLeaderboard { - private static final long serialVersionUID = 2370461218294770084L; - private final Regatta regatta; - - public RegattaLeaderboardImpl(Regatta regatta, ThresholdBasedResultDiscardingRule resultDiscardingRule) { - super(resultDiscardingRule); - this.regatta = regatta; - regatta.addRaceColumnListener(this); - } - - /** - * Updates the display name of this regatta leaderboard so that the regatta's name is no longer used as the default name. - */ - @Override - public void setName(String newName) { - setDisplayName(newName); - } - - @Override - public Regatta getRegatta() { - return regatta; - } - - @Override - public String getName() { - return getRegatta().getName(); - } - - @Override - public Iterable getRaceColumns() { - List result = new ArrayList(); - for (Series series : getRegatta().getSeries()) { - for (RaceColumn raceColumn : series.getRaceColumns()) { - result.add(raceColumn); - } - } - return result; - } - - @Override - public RaceColumnInSeries getRaceColumnByName(String columnName) { - return (RaceColumnInSeriesImpl) super.getRaceColumnByName(columnName); - } - - @Override - public ScoringScheme getScoringScheme() { - return regatta.getScoringScheme(); - } - - @Override - public CourseArea getDefaultCourseArea() { - return regatta.getDefaultCourseArea(); - } - - /** - * If the regatta' series {@link Regatta#definesSeriesDiscardThresholds() define} their own result discarding rules, this leaderboard uses - * a composite result discarding rule of type {@link PerSeriesResultDiscardingRuleImpl} that contains discards within each series according - * to the series' discarding rules. Otherwise, the default cross-leaderboard result discarding rule is obtained from the super class - * implementation. - */ - @Override - public ResultDiscardingRule getResultDiscardingRule() { - if (regatta.definesSeriesDiscardThresholds()) { - return new PerSeriesResultDiscardingRuleImpl(regatta); - } else { - return super.getResultDiscardingRule(); - } - } - - /** - * Delegates to {@link Regatta#getAllCompetitors()} which is expected to deliver all competitors from this - * leaderboard's regatta which includes those belonging to {@link RaceDefinition races} - * {@link Regatta#getAllRaces() belonging to the regatta} as well as competitors listed on the regatta's - * {@link RegattaLog}. - */ - @Override - public Iterable getAllCompetitors() { - return regatta.getAllCompetitors(); - } - - @Override - public IsRegattaLike getRegattaLike() { - return regatta; - } -} +package com.sap.sailing.domain.leaderboard.impl; + +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnInSeries; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.impl.RaceColumnInSeriesImpl; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.ResultDiscardingRule; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.regattalike.IsRegattaLike; +import com.sap.sailing.domain.tracking.TrackedRace; + +/** + * A leaderboard that is based on the definition of a {@link Regatta} with its {@link Series} and {@link Fleet}. The regatta + * leaderboard listens to its {@link Regatta} as a {@link RaceColumnListener} and forwards all link/unlink events for + * {@link TrackedRace}s being linked to / unlinked from race columns to all {@link RaceColumnListener}s subscribed with + * this leaderboard. + * + * @author Axel Uhl (D043530) + * + */ +public class RegattaLeaderboardImpl extends AbstractLeaderboardImpl implements RegattaLeaderboard { + private static final long serialVersionUID = 2370461218294770084L; + private final Regatta regatta; + + public RegattaLeaderboardImpl(Regatta regatta, ThresholdBasedResultDiscardingRule resultDiscardingRule) { + super(resultDiscardingRule); + this.regatta = regatta; + regatta.addRaceColumnListener(this); + } + + /** + * Updates the display name of this regatta leaderboard so that the regatta's name is no longer used as the default name. + */ + @Override + public void setName(String newName) { + setDisplayName(newName); + } + + @Override + public Regatta getRegatta() { + return regatta; + } + + @Override + public String getName() { + return getRegatta().getName(); + } + + @Override + public Iterable getRaceColumns() { + List result = new ArrayList(); + for (Series series : getRegatta().getSeries()) { + for (RaceColumn raceColumn : series.getRaceColumns()) { + result.add(raceColumn); + } + } + return result; + } + + @Override + public RaceColumnInSeries getRaceColumnByName(String columnName) { + return (RaceColumnInSeriesImpl) super.getRaceColumnByName(columnName); + } + + @Override + public ScoringScheme getScoringScheme() { + return regatta.getScoringScheme(); + } + + @Override + public CourseArea getDefaultCourseArea() { + return regatta.getDefaultCourseArea(); + } + + /** + * If the regatta' series {@link Regatta#definesSeriesDiscardThresholds() define} their own result discarding rules, this leaderboard uses + * a composite result discarding rule of type {@link PerSeriesResultDiscardingRuleImpl} that contains discards within each series according + * to the series' discarding rules. Otherwise, the default cross-leaderboard result discarding rule is obtained from the super class + * implementation. + */ + @Override + public ResultDiscardingRule getResultDiscardingRule() { + if (regatta.definesSeriesDiscardThresholds()) { + return new PerSeriesResultDiscardingRuleImpl(regatta); + } else { + return super.getResultDiscardingRule(); + } + } + + /** + * Delegates to {@link Regatta#getAllCompetitors()} which is expected to deliver all competitors from this + * leaderboard's regatta which includes those belonging to {@link RaceDefinition races} + * {@link Regatta#getAllRaces() belonging to the regatta} as well as competitors listed on the regatta's + * {@link RegattaLog}. + */ + @Override + public Iterable getAllCompetitors() { + return regatta.getAllCompetitors(); + } + + @Override + public IsRegattaLike getRegattaLike() { + return regatta; + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.RegattaLeaderboard; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java index a7de5eb4c40..e7f9c04de2d 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/FlexibleMetaLeaderboard.java @@ -1,64 +1,70 @@ -package com.sap.sailing.domain.leaderboard.meta; - -import java.util.ArrayList; -import java.util.List; - -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sse.concurrent.LockUtil; -import com.sap.sse.concurrent.NamedReentrantReadWriteLock; - -public class FlexibleMetaLeaderboard extends AbstractMetaLeaderboard { - private static final long serialVersionUID = 789076326144062944L; - - private final List leaderboards; - - private final NamedReentrantReadWriteLock leaderboardsLock; - - public FlexibleMetaLeaderboard(String name, ScoringScheme scoringScheme, - ThresholdBasedResultDiscardingRule resultDiscardingRule) { - super(name, scoringScheme, resultDiscardingRule); - leaderboards = new ArrayList(); - leaderboardsLock = new NamedReentrantReadWriteLock("leaderboards collection of "+FlexibleMetaLeaderboard.class.getSimpleName()+" "+getName(), - /* fair */ false); - } - - @Override - public Iterable getLeaderboards() { - LockUtil.lockForRead(leaderboardsLock); - try { - return new ArrayList(leaderboards); - } finally { - LockUtil.unlockAfterRead(leaderboardsLock); - } - } - - public void addLeaderboard(Leaderboard leaderboard) { - LockUtil.lockForWrite(leaderboardsLock); - try { - leaderboards.add(leaderboard); - registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); - } finally { - LockUtil.unlockAfterWrite(leaderboardsLock); - } - } - - public void removeLeaderboard(Leaderboard leaderboard) { - LockUtil.lockForWrite(leaderboardsLock); - try { - leaderboards.remove(leaderboard); - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); - unregisterScoreCorrectionChangeForwarder(leaderboard); - } finally { - LockUtil.unlockAfterWrite(leaderboardsLock); - } - } - - @Override - public CourseArea getDefaultCourseArea() { - return null; - } -} +package com.sap.sailing.domain.leaderboard.meta; + +import java.util.ArrayList; +import java.util.List; + +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sse.concurrent.LockUtil; +import com.sap.sse.concurrent.NamedReentrantReadWriteLock; + +public class FlexibleMetaLeaderboard extends AbstractMetaLeaderboard { + private static final long serialVersionUID = 789076326144062944L; + + private final List leaderboards; + + private final NamedReentrantReadWriteLock leaderboardsLock; + + public FlexibleMetaLeaderboard(String name, ScoringScheme scoringScheme, + ThresholdBasedResultDiscardingRule resultDiscardingRule) { + super(name, scoringScheme, resultDiscardingRule); + leaderboards = new ArrayList(); + leaderboardsLock = new NamedReentrantReadWriteLock("leaderboards collection of "+FlexibleMetaLeaderboard.class.getSimpleName()+" "+getName(), + /* fair */ false); + } + + @Override + public Iterable getLeaderboards() { + LockUtil.lockForRead(leaderboardsLock); + try { + return new ArrayList(leaderboards); + } finally { + LockUtil.unlockAfterRead(leaderboardsLock); + } + } + + public void addLeaderboard(Leaderboard leaderboard) { + LockUtil.lockForWrite(leaderboardsLock); + try { + leaderboards.add(leaderboard); + registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); + } finally { + LockUtil.unlockAfterWrite(leaderboardsLock); + } + } + + public void removeLeaderboard(Leaderboard leaderboard) { + LockUtil.lockForWrite(leaderboardsLock); + try { + leaderboards.remove(leaderboard); + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); + unregisterScoreCorrectionChangeForwarder(leaderboard); + } finally { + LockUtil.unlockAfterWrite(leaderboardsLock); + } + } + + @Override + public CourseArea getDefaultCourseArea() { + return null; + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.FlexibleMetaLeaderboard; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java index 73860657959..673e1ee5120 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/meta/LeaderboardGroupMetaLeaderboard.java @@ -1,92 +1,98 @@ -package com.sap.sailing.domain.leaderboard.meta; - -import com.sap.sailing.domain.base.CourseArea; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.RaceColumnListener; -import com.sap.sailing.domain.base.impl.TrackedRaces; -import com.sap.sailing.domain.common.LeaderboardNameConstants; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.leaderboard.LeaderboardGroup; -import com.sap.sailing.domain.leaderboard.LeaderboardGroupListener; -import com.sap.sailing.domain.leaderboard.ScoringScheme; -import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; -import com.sap.sailing.domain.tracking.TrackedRace; - -/** - * A meta leaderboard which considers all regular leaderboards of a {@link LeaderboardGroup}. To stay up to date, this - * meta leaderboard registers itself as a {@link LeaderboardGroupListener} on the leaderboard group it represents. This - * way, whenever a leaderboard is added to or removed from the group, this meta leaderboard can in turn inform its - * {@link RaceColumnListener}s about the impact this change has on the set of {@link TrackedRaces} and therefore the - * competitors attached to this meta leaderboard. - *

    - * - * After an object of this type has been de-serialized, and after all objects referenced by it (leaderboard, leaderboard - * group) have been initialized, {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} must be called to - * ensure that score corrections are propagated as desired. This is because score correction listeners are "transient" - * and are as such not serialized. Note: calling {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} - * during readObject or readResolve is not possible because the object graph hasn't been fully - * initialized yet when those methods are called. Hence, the structures for registering the score correction listeners - * are not yet in place at that time. - * - * @author Axel Uhl (d043530) - * - */ -public class LeaderboardGroupMetaLeaderboard extends AbstractMetaLeaderboard implements LeaderboardGroupListener, RaceColumnListener { - private static final long serialVersionUID = 8087872002175528002L; - - private final LeaderboardGroup leaderboardGroup; - - public LeaderboardGroupMetaLeaderboard(LeaderboardGroup leaderboardGroup, ScoringScheme scoringScheme, - ThresholdBasedResultDiscardingRule resultDiscardingRule) { - super(leaderboardGroup.getName() + " " + LeaderboardNameConstants.OVERALL, scoringScheme, resultDiscardingRule); - this.leaderboardGroup = leaderboardGroup; - leaderboardGroup.addLeaderboardGroupListener(this); - registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards(); - } - - public void registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards() { - for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { - registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); - } - } - - @Override - public Iterable getLeaderboards() { - return leaderboardGroup.getLeaderboards(); - } - - @Override - public void leaderboardAdded(LeaderboardGroup group, Leaderboard leaderboard) { - registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); - } - } - } - getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); - } - - @Override - public void leaderboardRemoved(LeaderboardGroup group, Leaderboard leaderboard) { - leaderboard.removeRaceColumnListener(this); - unregisterScoreCorrectionChangeForwarder(leaderboard); - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - for (Fleet fleet : raceColumn.getFleets()) { - TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); - } - } - } - getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); - } - - @Override - public CourseArea getDefaultCourseArea() { - return null; - } -} +package com.sap.sailing.domain.leaderboard.meta; + +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnListener; +import com.sap.sailing.domain.base.impl.TrackedRaces; +import com.sap.sailing.domain.common.LeaderboardNameConstants; +import com.sap.sailing.domain.common.LeaderboardType; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.leaderboard.LeaderboardGroupListener; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.ThresholdBasedResultDiscardingRule; +import com.sap.sailing.domain.tracking.TrackedRace; + +/** + * A meta leaderboard which considers all regular leaderboards of a {@link LeaderboardGroup}. To stay up to date, this + * meta leaderboard registers itself as a {@link LeaderboardGroupListener} on the leaderboard group it represents. This + * way, whenever a leaderboard is added to or removed from the group, this meta leaderboard can in turn inform its + * {@link RaceColumnListener}s about the impact this change has on the set of {@link TrackedRaces} and therefore the + * competitors attached to this meta leaderboard. + *

    + * + * After an object of this type has been de-serialized, and after all objects referenced by it (leaderboard, leaderboard + * group) have been initialized, {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} must be called to + * ensure that score corrections are propagated as desired. This is because score correction listeners are "transient" + * and are as such not serialized. Note: calling {@link #registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards()} + * during readObject or readResolve is not possible because the object graph hasn't been fully + * initialized yet when those methods are called. Hence, the structures for registering the score correction listeners + * are not yet in place at that time. + * + * @author Axel Uhl (d043530) + * + */ +public class LeaderboardGroupMetaLeaderboard extends AbstractMetaLeaderboard implements LeaderboardGroupListener, RaceColumnListener { + private static final long serialVersionUID = 8087872002175528002L; + + private final LeaderboardGroup leaderboardGroup; + + public LeaderboardGroupMetaLeaderboard(LeaderboardGroup leaderboardGroup, ScoringScheme scoringScheme, + ThresholdBasedResultDiscardingRule resultDiscardingRule) { + super(leaderboardGroup.getName() + " " + LeaderboardNameConstants.OVERALL, scoringScheme, resultDiscardingRule); + this.leaderboardGroup = leaderboardGroup; + leaderboardGroup.addLeaderboardGroupListener(this); + registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards(); + } + + public void registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards() { + for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { + registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); + } + } + + @Override + public Iterable getLeaderboards() { + return leaderboardGroup.getLeaderboards(); + } + + @Override + public void leaderboardAdded(LeaderboardGroup group, Leaderboard leaderboard) { + registerScoreCorrectionAndRaceColumnChangeForwarder(leaderboard); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceLinked(raceColumn, fleet, trackedRace); + } + } + } + getRaceColumnListeners().notifyListenersAboutRaceColumnAddedToContainer(getColumnForLeaderboard(leaderboard)); + } + + @Override + public void leaderboardRemoved(LeaderboardGroup group, Leaderboard leaderboard) { + leaderboard.removeRaceColumnListener(this); + unregisterScoreCorrectionChangeForwarder(leaderboard); + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + if (trackedRace != null) { + getRaceColumnListeners().notifyListenersAboutTrackedRaceUnlinked(raceColumn, fleet, trackedRace); + } + } + } + getRaceColumnListeners().notifyListenersAboutRaceColumnRemovedFromContainer(getColumnForLeaderboard(leaderboard)); + } + + @Override + public CourseArea getDefaultCourseArea() { + return null; + } + + @Override + protected LeaderboardType getLeaderboardType() { + return LeaderboardType.RegattaMetaLeaderboard; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index db9cbbe03cc..b9d964fc98e 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -13,7 +13,6 @@ import java.util.ConcurrentModificationException; import java.util.Date; import java.util.HashMap; import java.util.HashSet; -import java.util.IdentityHashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; @@ -154,6 +153,7 @@ import com.sap.sse.common.Util.Pair; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.concurrent.LockUtil; import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.util.IdentityWrapper; import com.sap.sse.util.SmartFutureCache; import com.sap.sse.util.SmartFutureCache.AbstractCacheUpdater; import com.sap.sse.util.SmartFutureCache.EmptyUpdateInterval; @@ -183,7 +183,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl * By default, all wind sources are used, none are excluded. However, e.g., for performance reasons, particular wind * sources such as the track-based estimation wind source, may be excluded by adding them to this set. */ - private final Set windSourcesToExclude; + private final ConcurrentHashMap windSourcesToExclude; /** * Keeps the oldest timestamp that is fed into this tracked race, either from a boat fix, a mark fix, a race @@ -269,7 +269,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl */ private transient SmartFutureCache>, EmptyUpdateInterval> maneuverCache; - private transient Map> directionFromStartToNextMarkCache; + private transient ConcurrentHashMap> directionFromStartToNextMarkCache; protected final MarkPassingCalculator markPassingCalculator; @@ -354,7 +354,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl */ private final NamedReentrantReadWriteLock loadingFromGPSFixStoreLock; - private final Map, NamedReentrantReadWriteLock> locksForMarkPassings; + private final ConcurrentHashMap>, NamedReentrantReadWriteLock> locksForMarkPassings; /** * Caches wind requests for a few seconds to accelerate access in live mode @@ -412,7 +412,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl rankingMetric = rankingMetricConstructor.apply(this); raceStates = new WeakHashMap<>(); shortTimeWindCache = new ShortTimeWindCache(this, millisecondsOverWhichToAverageWind / 2); - locksForMarkPassings = new IdentityHashMap<>(); + locksForMarkPassings = new ConcurrentHashMap>, NamedReentrantReadWriteLock>(); attachedRaceLogs = new ConcurrentHashMap<>(); attachedRegattaLogs = new ConcurrentHashMap<>(); attachedRaceExecutionOrderProviders = new ConcurrentHashMap<>(); @@ -424,8 +424,8 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl + race.getName(), /* fair */ false); this.cacheInvalidationTimerLock = new Object(); this.updateCount = 0; - this.windSourcesToExclude = new HashSet(); - this.directionFromStartToNextMarkCache = new HashMap>(); + this.windSourcesToExclude = new ConcurrentHashMap<>(); + this.directionFromStartToNextMarkCache = new ConcurrentHashMap>(); this.millisecondsOverWhichToAverageSpeed = millisecondsOverWhichToAverageSpeed; this.delayToLiveInMillis = delayToLiveInMillis; this.startToNextMarkCacheInvalidationListeners = new ConcurrentHashMap(); @@ -624,7 +624,7 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl gpsFixStore = EmptyGPSFixStore.INSTANCE; competitorRankings = createCompetitorRankingsCache(); competitorRankingsLocks = createCompetitorRankingsLockMap(); - directionFromStartToNextMarkCache = new HashMap>(); + directionFromStartToNextMarkCache = new ConcurrentHashMap<>(); crossTrackErrorCache = new CrossTrackErrorCache(this); crossTrackErrorCache.invalidate(); maneuverCache = createManeuverCache(); @@ -1146,11 +1146,11 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl int indexOfWaypoint = getRace().getCourse().getIndexOfWaypoint(startOfLeg); if (indexOfWaypoint == -1) { throw new IllegalArgumentException("Waypoint " + startOfLeg + " not found in " + getRace().getCourse()); - } else if (indexOfWaypoint == Util.size(getRace().getCourse().getWaypoints()) - 1) { + } else if (indexOfWaypoint == getRace().getCourse().getNumberOfWaypoints() - 1) { throw new IllegalArgumentException("Waypoint " + startOfLeg + " isn't start of any leg in " + getRace().getCourse()); } - return trackedLegs.get(race.getCourse().getLegs().get(indexOfWaypoint)); + return trackedLegs.get(race.getCourse().getLeg(indexOfWaypoint)); } finally { getRace().getCourse().unlockAfterRead(); } @@ -1679,24 +1679,20 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl @Override public Set getWindSourcesToExclude() { - synchronized (windSourcesToExclude) { - return Collections.unmodifiableSet(windSourcesToExclude); - } + return Collections.unmodifiableSet(windSourcesToExclude.keySet()); } @Override public void setWindSourcesToExclude(Iterable windSourcesToExclude) { - Set old = new HashSet<>(this.windSourcesToExclude); - synchronized (this.windSourcesToExclude) { - LockUtil.lockForRead(getSerializationLock()); - try { - this.windSourcesToExclude.clear(); - for (WindSource windSourceToExclude : windSourcesToExclude) { - this.windSourcesToExclude.add(windSourceToExclude); - } - } finally { - LockUtil.unlockAfterRead(getSerializationLock()); + Set old = new HashSet<>(getWindSourcesToExclude()); + LockUtil.lockForRead(getSerializationLock()); + try { + this.windSourcesToExclude.clear(); + for (WindSource windSourceToExclude : windSourcesToExclude) { + this.windSourcesToExclude.put(windSourceToExclude, this); } + } finally { + LockUtil.unlockAfterRead(getSerializationLock()); } if (!old.equals(this.windSourcesToExclude)) { clearAllCachesExceptManeuvers(); @@ -1742,30 +1738,33 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl public Wind getDirectionFromStartToNextMark(final TimePoint at) { Future future; FutureTask newFuture = null; - synchronized (directionFromStartToNextMarkCache) { - future = directionFromStartToNextMarkCache.get(at); - if (future == null) { - newFuture = new FutureTask(new Callable() { - @Override - public Wind call() { - Wind result; - Leg firstLeg = getRace().getCourse().getFirstLeg(); - if (firstLeg != null) { - Position firstLegEnd = getApproximatePosition(firstLeg.getTo(), at); - Position firstLegStart = getApproximatePosition(firstLeg.getFrom(), at); - if (firstLegStart != null && firstLegEnd != null) { - result = new WindImpl(firstLegStart, at, new KnotSpeedWithBearingImpl(0.0, - firstLegEnd.getBearingGreatCircle(firstLegStart))); + future = directionFromStartToNextMarkCache.get(at); + if (future == null) { + synchronized (directionFromStartToNextMarkCache) { + future = directionFromStartToNextMarkCache.get(at); + if (future == null) { + newFuture = new FutureTask(new Callable() { + @Override + public Wind call() { + Wind result; + Leg firstLeg = getRace().getCourse().getFirstLeg(); + if (firstLeg != null) { + Position firstLegEnd = getApproximatePosition(firstLeg.getTo(), at); + Position firstLegStart = getApproximatePosition(firstLeg.getFrom(), at); + if (firstLegStart != null && firstLegEnd != null) { + result = new WindImpl(firstLegStart, at, new KnotSpeedWithBearingImpl(0.0, + firstLegEnd.getBearingGreatCircle(firstLegStart))); + } else { + result = null; + } } else { result = null; } - } else { - result = null; + return result; } - return result; - } - }); - directionFromStartToNextMarkCache.put(at, newFuture); + }); + directionFromStartToNextMarkCache.put(at, newFuture); + } } } if (newFuture != null) { @@ -2062,15 +2061,19 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl } protected NamedReentrantReadWriteLock getMarkPassingsLock(Iterable markPassings) { - synchronized (locksForMarkPassings) { - NamedReentrantReadWriteLock lock = locksForMarkPassings.get(markPassings); - if (lock == null) { - lock = new NamedReentrantReadWriteLock("mark passings lock for tracked race " + getRace().getName(), /* fair */ - false); - locksForMarkPassings.put(markPassings, lock); + final IdentityWrapper> markPassingsIdentity = new IdentityWrapper<>(markPassings); + NamedReentrantReadWriteLock lock = locksForMarkPassings.get(markPassingsIdentity); + if (lock == null) { + synchronized (locksForMarkPassings) { + lock = locksForMarkPassings.get(markPassingsIdentity); + if (lock == null) { + lock = new NamedReentrantReadWriteLock( + "mark passings lock for tracked race " + getRace().getName(), /* fair */false); + locksForMarkPassings.put(markPassingsIdentity, lock); + } } - return lock; } + return lock; } private void updateStartToNextMarkCacheInvalidationCacheListenersAfterWaypointRemoved(int zeroBasedIndex, diff --git a/java/com.sap.sailing.feature.p2build/raceanalysis.product b/java/com.sap.sailing.feature.p2build/raceanalysis.product index b1682e26616..1dead91f56d 100755 --- a/java/com.sap.sailing.feature.p2build/raceanalysis.product +++ b/java/com.sap.sailing.feature.p2build/raceanalysis.product @@ -50,6 +50,7 @@ + diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java index f9c0928e151..c6ef63bfb48 100644 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/GwtTestCaseColumnToggling.java @@ -95,7 +95,7 @@ public class GwtTestCaseColumnToggling extends GWTTestCase { + TracTracConnectionConstants.PORT_TUNNEL_STORED : "tcp://" + TracTracConnectionConstants.HOST_NAME + ":" + TracTracConnectionConstants.PORT_STORED, COURSE_DESIGN_UPDATE_URI, - false, false, /* simulateWithStartTimeNow */ false, /* ignoreTracTracMarkPassings */ false, TRACTRAC_USERNAME, TRACTRAC_PASSWORD, new AsyncCallback() { + false, false, /* offsetToStartTimeOfSimulatedRace */ null, /* ignoreTracTracMarkPassings */ false, TRACTRAC_USERNAME, TRACTRAC_PASSWORD, new AsyncCallback() { @Override public void onFailure(Throwable caught) { diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java index 4999b3d2c9f..38c0fbdb061 100644 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/LeaderboardPanelMock.java @@ -20,7 +20,7 @@ public class LeaderboardPanelMock extends LeaderboardPanel { String leaderboardName, ErrorReporter errorReporter, StringMessages stringMessages) { super(sailingService, new AsyncActionsExecutor(), LeaderboardSettingsFactory.getInstance().createNewDefaultSettings( - /* racesToShow */null, /* namesOfRacesToShow */ null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true), new CompetitorSelectionModel( + /* racesToShow */null, /* namesOfRacesToShow */ null, null, /* autoExpandFirstRace */ false, /* showRegattaRank */ true, /* showCompetitorSailIdColumn */ true, /* showCompetitorFullNameColumn */ true), new CompetitorSelectionModel( /* hasMultiSelection */true), leaderboardName, errorReporter, stringMessages, new UserAgentDetails("gecko1_8"), /* showRaceDetails */ true); } diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java index 38860207a43..a6123d06096 100755 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/TestColumnSwapping.java @@ -1,162 +1,162 @@ -package com.sap.sailing.gwt.ui.test; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import org.junit.Before; -import org.junit.Test; - -import com.sap.sailing.domain.common.ScoringSchemeType; -import com.sap.sailing.domain.common.dto.FleetDTO; -import com.sap.sailing.domain.common.dto.LeaderboardDTO; -import com.sap.sailing.domain.common.dto.RaceColumnDTO; -import com.sap.sailing.gwt.ui.server.SailingServiceImpl; - -public class TestColumnSwapping { - - private LeaderboardDTO lb = null; - private static final String LEADERBOARDNAME = "test"; - private static final String DEFAULT_FLEET_NAME = "Default"; - private static final FleetDTO DEFAULT_FLEET = new FleetDTO(DEFAULT_FLEET_NAME, /* ordering */ 0, /* color */ null); - private SailingServiceImpl service; - private LeaderboardDTO leaderboardOriginalDTO; - private LeaderboardDTO leaderboardDTO; - private Collection leglist; - private Date leaderboardCreationDate; - private SailingServiceImpl sailingService = null; - - // Test-Data - private final String TEST_LEADERBOARD_NAME = "test_board"; - private final String[] races = { "1", "2", "3" }; - private final boolean[] isMedalRace = { false, false, true }; - - @Before - public void prepareColumnSwapping() { - service = new SailingServiceImplMock(); - int[] disc = { 5, 8, 9, 0, 7, 5, 43 }; - service.removeLeaderboard(LEADERBOARDNAME); - service.createFlexibleLeaderboard(LEADERBOARDNAME, null, disc, ScoringSchemeType.LOW_POINT, null); - service.addColumnToLeaderboard("Race1", LEADERBOARDNAME, true); - service.addColumnToLeaderboard("Race2", LEADERBOARDNAME, true); - service.addColumnToLeaderboard("Race3", LEADERBOARDNAME, true); - leglist = new ArrayList(); - leglist.add("Race1"); - leglist.add("Race2"); - leglist.add("Race3"); - leaderboardCreationDate = new Date(); - try { - // get Leaderboard with name and current date - leaderboardOriginalDTO = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { - @Override - public String generateRandomUUID() { - return UUID.randomUUID().toString(); - } - }, /* hasOverallDetails */ false); - leaderboardOriginalDTO.addRace("Race1", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - leaderboardOriginalDTO.addRace("Race3", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - leaderboardOriginalDTO.addRace("Race2", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Test - public void testColumnSwapping() { - testColumnSwappingFabian(); - testLeaderBoardDTOMethods(); - testSailingService(); - } - - @Test - public void testSailingService() { - sailingService = new SailingServiceImplMock(); - assertNotNull("Sailingservice != NULL", sailingService); - int td[] = { 5, 8 }; - sailingService.removeLeaderboard(TEST_LEADERBOARD_NAME); - sailingService.createFlexibleLeaderboard(TEST_LEADERBOARD_NAME, null, td, ScoringSchemeType.LOW_POINT, null); - for (int i = 0; i < races.length; i++) - sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - sailingService.moveLeaderboardColumnUp(TEST_LEADERBOARD_NAME, races[2]); - sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true); - sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false); - try { - for (int i = 0; i < races.length; i++) - sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); - sailingService.moveLeaderboardColumnUp(TEST_LEADERBOARD_NAME, races[2]); - sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true); - sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false); - lb = sailingService.getLeaderboardByName(TEST_LEADERBOARD_NAME, new Date(), /* races to load */ null, /* addOverallDetails */ true, /* previous leaderboard ID */ null, - /* fillNetPointsUncorrected */ false).getLeaderboardDTO(/* previousVersion */ null); - } catch (Exception e) { - // e.printStackTrace(); - fail(e.getLocalizedMessage()); - } - assertNotNull("LeaderboardDTO != NULL", lb); - - // asserted data - String[] raceNames = new String[] { "3", "2", "1" }; - boolean[] medalRace = { true, false, false }; - - for (int i = 0; i < lb.getRaceList().size(); i++) { - assertEquals("Race[" + i + "] == " + raceNames[i], raceNames[i], lb.getRaceList().get(i).getName()); - assertTrue("Race[" + i + "] is " + (medalRace[i] ? "" : "no ") + "medalrace.", - lb.raceIsMedalRace(races[i]) == medalRace[i]); - } - } - - @Test - public void testLeaderBoardDTOMethods() { - lb = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { - @Override - public String generateRandomUUID() { - return UUID.randomUUID().toString(); - } - }, /* hasOverallDetails */ false); - assertNotNull("Leaderboard != NULL", lb); - lb.addRace("1", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null); - lb.addRace("2", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null); - lb.addRace("3", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null); - lb.moveRaceDown("1"); - String[] s = new String[] { "2", "1", "3" }; - for (int i = 0; i < lb.getRaceList().size(); i++) { - assertEquals("Race[" + i + "] = " + s[i], s[i], lb.getRaceList().get(i).getName()); - } - } - - @Test - public void testColumnSwappingFabian() { - service.moveLeaderboardColumnUp(LEADERBOARDNAME, "Race3"); - try { - leaderboardDTO = service.getLeaderboardByName(LEADERBOARDNAME, leaderboardCreationDate, leglist, /* addOverallDetails */ - true, /* previous leaderboard ID */null, /* fillNetPointsUncorrected */false).getLeaderboardDTO(/* previousVersion */null); - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - // check if leaderboardDTO an dleaderboardOriginalDTO same - List leaderboardList = leaderboardDTO.getRaceList(); - List leaderboardOriginalList = leaderboardOriginalDTO.getRaceList(); - - // ??????? assert races in list - assertArrayEquals(leaderboardList.toArray(), leaderboardOriginalList.toArray()); - - for (RaceColumnDTO raceDTO : leaderboardOriginalList) { - assert leaderboardDTO.raceIsMedalRace(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsMedalRace(raceDTO.getRaceColumnName()); - assert leaderboardDTO.raceIsTracked(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsTracked(raceDTO.getRaceColumnName()); - } - } -} +package com.sap.sailing.gwt.ui.test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; +import com.sap.sailing.gwt.ui.server.SailingServiceImpl; + +public class TestColumnSwapping { + + private LeaderboardDTO lb = null; + private static final String LEADERBOARDNAME = "test"; + private static final String DEFAULT_FLEET_NAME = "Default"; + private static final FleetDTO DEFAULT_FLEET = new FleetDTO(DEFAULT_FLEET_NAME, /* ordering */ 0, /* color */ null); + private SailingServiceImpl service; + private LeaderboardDTO leaderboardOriginalDTO; + private LeaderboardDTO leaderboardDTO; + private Collection leglist; + private Date leaderboardCreationDate; + private SailingServiceImpl sailingService = null; + + // Test-Data + private final String TEST_LEADERBOARD_NAME = "test_board"; + private final String[] races = { "1", "2", "3" }; + private final boolean[] isMedalRace = { false, false, true }; + + @Before + public void prepareColumnSwapping() { + service = new SailingServiceImplMock(); + int[] disc = { 5, 8, 9, 0, 7, 5, 43 }; + service.removeLeaderboard(LEADERBOARDNAME); + service.createFlexibleLeaderboard(LEADERBOARDNAME, null, disc, ScoringSchemeType.LOW_POINT, null); + service.addColumnToLeaderboard("Race1", LEADERBOARDNAME, true); + service.addColumnToLeaderboard("Race2", LEADERBOARDNAME, true); + service.addColumnToLeaderboard("Race3", LEADERBOARDNAME, true); + leglist = new ArrayList(); + leglist.add("Race1"); + leglist.add("Race2"); + leglist.add("Race3"); + leaderboardCreationDate = new Date(); + try { + // get Leaderboard with name and current date + leaderboardOriginalDTO = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { + @Override + public String generateRandomUUID() { + return UUID.randomUUID().toString(); + } + }, /* hasOverallDetails */ false); + leaderboardOriginalDTO.addRace("Race1", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + leaderboardOriginalDTO.addRace("Race3", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + leaderboardOriginalDTO.addRace("Race2", /* explicitFactor */ null, 2., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Test + public void testColumnSwapping() { + testColumnSwappingFabian(); + testLeaderBoardDTOMethods(); + testSailingService(); + } + + @Test + public void testSailingService() { + sailingService = new SailingServiceImplMock(); + assertNotNull("Sailingservice != NULL", sailingService); + int td[] = { 5, 8 }; + sailingService.removeLeaderboard(TEST_LEADERBOARD_NAME); + sailingService.createFlexibleLeaderboard(TEST_LEADERBOARD_NAME, null, td, ScoringSchemeType.LOW_POINT, null); + for (int i = 0; i < races.length; i++) + sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + sailingService.moveLeaderboardColumnUp(TEST_LEADERBOARD_NAME, races[2]); + sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true); + sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false); + try { + for (int i = 0; i < races.length; i++) + sailingService.addColumnToLeaderboard(races[i], TEST_LEADERBOARD_NAME, isMedalRace[i]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + sailingService.moveLeaderboardColumnDown(TEST_LEADERBOARD_NAME, races[0]); + sailingService.moveLeaderboardColumnUp(TEST_LEADERBOARD_NAME, races[2]); + sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[0], true); + sailingService.updateIsMedalRace(TEST_LEADERBOARD_NAME, races[2], false); + lb = sailingService.getLeaderboardByName(TEST_LEADERBOARD_NAME, new Date(), /* races to load */ null, /* addOverallDetails */ true, /* previous leaderboard ID */ null, + /* fillNetPointsUncorrected */ false).getLeaderboardDTO(/* previousVersion */ null); + } catch (Exception e) { + // e.printStackTrace(); + fail(e.getLocalizedMessage()); + } + assertNotNull("LeaderboardDTO != NULL", lb); + + // asserted data + String[] raceNames = new String[] { "3", "2", "1" }; + boolean[] medalRace = { true, false, false }; + + for (int i = 0; i < lb.getRaceList().size(); i++) { + assertEquals("Race[" + i + "] == " + raceNames[i], raceNames[i], lb.getRaceList().get(i).getName()); + assertTrue("Race[" + i + "] is " + (medalRace[i] ? "" : "no ") + "medalrace.", + lb.raceIsMedalRace(races[i]) == medalRace[i]); + } + } + + @Test + public void testLeaderBoardDTOMethods() { + lb = new LeaderboardDTO(null, null, ScoringSchemeType.LOW_POINT, /* higherScoreIsBetter */ false, new LeaderboardDTO.UUIDGenerator() { + @Override + public String generateRandomUUID() { + return UUID.randomUUID().toString(); + } + }, /* hasOverallDetails */ false); + assertNotNull("Leaderboard != NULL", lb); + lb.addRace("1", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null, false); + lb.addRace("2", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, false, null, null, false); + lb.addRace("3", /* explicitFactor */ null, 1., /* regattaName */ null, /* seriesName */ null, DEFAULT_FLEET, true, null, null, false); + lb.moveRaceDown("1"); + String[] s = new String[] { "2", "1", "3" }; + for (int i = 0; i < lb.getRaceList().size(); i++) { + assertEquals("Race[" + i + "] = " + s[i], s[i], lb.getRaceList().get(i).getName()); + } + } + + @Test + public void testColumnSwappingFabian() { + service.moveLeaderboardColumnUp(LEADERBOARDNAME, "Race3"); + try { + leaderboardDTO = service.getLeaderboardByName(LEADERBOARDNAME, leaderboardCreationDate, leglist, /* addOverallDetails */ + true, /* previous leaderboard ID */null, /* fillNetPointsUncorrected */false).getLeaderboardDTO(/* previousVersion */null); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + // check if leaderboardDTO an dleaderboardOriginalDTO same + List leaderboardList = leaderboardDTO.getRaceList(); + List leaderboardOriginalList = leaderboardOriginalDTO.getRaceList(); + + // ??????? assert races in list + assertArrayEquals(leaderboardList.toArray(), leaderboardOriginalList.toArray()); + + for (RaceColumnDTO raceDTO : leaderboardOriginalList) { + assert leaderboardDTO.raceIsMedalRace(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsMedalRace(raceDTO.getRaceColumnName()); + assert leaderboardDTO.raceIsTracked(raceDTO.getRaceColumnName()) == leaderboardOriginalDTO.raceIsTracked(raceDTO.getRaceColumnName()); + } + } +} diff --git a/java/com.sap.sailing.gwt.ui/Leaderboard.css b/java/com.sap.sailing.gwt.ui/Leaderboard.css index 8ab68acf9b9..ca7ab51f790 100755 --- a/java/com.sap.sailing.gwt.ui/Leaderboard.css +++ b/java/com.sap.sailing.gwt.ui/Leaderboard.css @@ -98,13 +98,13 @@ body, table td, select, button { padding: 49px 0 0 80px; width: 300px; color: #7c7c7c; - background: url(images/important-message.png) left center no-repeat; + background: url(images/important-message.png) left center no-repeat; } .LeaderboardHeader { position: relative; margin: 0 auto; - width: 400px; + width: 100%; text-align: center; font-family: 'Open Sans', Arial, Verdana, sans-serif; font-size: 18px; diff --git a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF index 91e8a38b32f..5eeb8e17321 100644 --- a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF @@ -49,9 +49,10 @@ Require-Bundle: com.sap.sailing.domain, org.apache.shiro.core;bundle-version="1.2.2", com.sap.sse.gwt.theme, com.sap.sailing.news, - com.sap.sailing.polars.datamining.shared;bundle-version="1.0.0", + com.sap.sailing.polars.datamining.shared, com.sap.sailing.polars.datamining, - com.sap.sse.datamining.annotations;bundle-version="1.0.0" + com.sap.sse.datamining.annotations, + com.sap.sse.shared.android Bundle-Activator: com.sap.sailing.gwt.ui.server.Activator Bundle-ActivationPolicy: lazy Import-Package: javax.servlet;version="3.1.0", diff --git a/java/com.sap.sailing.gwt.ui/RaceBoard.css b/java/com.sap.sailing.gwt.ui/RaceBoard.css index 0e7cd4f163c..0175c2a97dd 100644 --- a/java/com.sap.sailing.gwt.ui/RaceBoard.css +++ b/java/com.sap.sailing.gwt.ui/RaceBoard.css @@ -6,6 +6,13 @@ height: 0; clear: both; visibility: hidden; + + RegattaRaceInformation-Header + RaceName-Label + RaceSeriesAndFleet-Label + + RegattaAndRaceTime-Header??? + RegattaName-Anchor */ @@ -87,6 +94,14 @@ table { padding-top: 10px; padding-left: 10px; } +.CombinedWindPanelParentDiv.indentsmall, +.TrueNorthIndicatorPanelParentDiv.indentsmall { + padding-left: 30px; +} +.CombinedWindPanelParentDiv.indentbig, +.TrueNorthIndicatorPanelParentDiv.indentbig { + padding-left: 45px; +} .MapSimulationLegendParentDiv { padding-bottom: 10px; @@ -573,7 +588,8 @@ img.openColumn { url(images/viewicons/@1x/img/SAP_RV_Leaderboard_GHOSTED.png), url(images/arrows_top_black_small.png); background-repeat: no-repeat, no-repeat; - background-position: 4% 50%, 96% 50% + background-position: 4% 50%, 96% 50%; + background-position: 5px center, calc(100% - 6px) center; } .gwt-SplitLayoutPanel-EastToggleButton-Closed-leaderboard { @@ -582,7 +598,8 @@ img.openColumn { url(images/viewicons/@1x/img/SAP_RV_Leaderboard_INACTIVE.png), url(images/arrows_bottom_black_small.png); background-repeat: no-repeat, no-repeat; - background-position: 4% 50%, 96% 50% + background-position: 4% 50%, 96% 50%; + background-position: 5px center, calc(100% - 6px) center; } .gwt-SplitLayoutPanel-NorthSouthToggleButton-Closed-media, @@ -664,6 +681,32 @@ img.openColumn { margin-top: 5px; } +.compactHeader .RegattaRaceInformation-Header { + left: 110px; +} +.compactHeader .RegattaRaceInformation-Header .RaceName-Label { + display: block; + font-size: 16px; + padding: 2px 0; +} +.compactHeader .RegattaRaceInformation-Header .RaceSeriesAndFleet-Label { + display: block; + margin-top: 5px; + white-space: nowrap; +} +.compactHeader .RegattaAndRaceTime-Header { + top: 11px; +} +.compactHeader .RegattaAndRaceTime-Header .RegattaName-Anchor { + width: 115px; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + white-space: nowrap; + padding: 2px 0 2px 20px; + background-position: 4px -1px; +} + .VideoPopup-Close-Button { border: none; } diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch index af074b8e973..5356d07a245 100644 --- a/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT sdm.launch @@ -17,7 +17,7 @@ - + @@ -73,7 +73,7 @@ - + diff --git a/java/com.sap.sailing.gwt.ui/SailingGWT.launch b/java/com.sap.sailing.gwt.ui/SailingGWT.launch index 4f94e217a34..b677fe5c939 100755 --- a/java/com.sap.sailing.gwt.ui/SailingGWT.launch +++ b/java/com.sap.sailing.gwt.ui/SailingGWT.launch @@ -16,7 +16,7 @@ - + @@ -70,7 +70,7 @@ - + diff --git a/java/com.sap.sailing.gwt.ui/pom.xml b/java/com.sap.sailing.gwt.ui/pom.xml index 8ff99a6b542..47a3ef69008 100755 --- a/java/com.sap.sailing.gwt.ui/pom.xml +++ b/java/com.sap.sailing.gwt.ui/pom.xml @@ -11,19 +11,6 @@ com.sap.sailing.gwt.ui eclipse-plugin - - - sonatype - http://oss.sonatype.org/content/repositories/snapshots - - true - - - false - - - - use all race logs - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - for (Fleet fleet : raceColumn.getFleets()) { - logs.add(raceColumn.getRaceLog(fleet)); - } - } - } else { - logs.add(raceLog); - } - for (AbstractLog log : logs) { - try { - getGPSFixStore().loadMarkTrack(loadedTrack, log, mark); - } catch (Exception e) { - logger.log(Level.WARNING, "Couldn't load mark track for mark " + mark + " from log " + log, e); - } - } - } - Position result = track.getEstimatedPosition(timePoint, /* extrapolate */ false); - if (result == null) { - result = nonSpanningFallback == null ? null : nonSpanningFallback.getPosition(); - } - return result; - } - - private GPSFix improveTimewiseClosestFix(GPSFix nonSpanningFallback, GPSFixTrack track, final TimePoint timePoint) { - GPSFix lastAtOrBefore = track.getLastFixAtOrBefore(timePoint); - GPSFix firstAtOrAfter = track.getFirstFixAtOrAfter(timePoint); - // find the fix closes to timePoint, sorting null values to the end and fixes near timePoint to the beginning - final List list = Arrays.asList(nonSpanningFallback, lastAtOrBefore, firstAtOrAfter); - list.sort(new Comparator() { - @Override - public int compare(GPSFix o1, GPSFix o2) { - final int result; - if (o1 == null) { - if (o2 == null) { - result = 0; - } else { - result = 1; - } - } else if (o2 == null) { - result = -1; - } else { - result = new Long(Math.abs(o1.getTimePoint().until(timePoint).asMillis())).compareTo( - Math.abs(o2.getTimePoint().until(timePoint).asMillis())); - } - return result; - } - }); - return list.get(0); - } - - private boolean spansTimePoint(GPSFixTrack track, TimePoint timePoint) { - return track.getLastFixAtOrBefore(timePoint) != null && track.getFirstFixAtOrAfter(timePoint) != null; - } - - @Override - public Map getLeaderboards() { - return Collections.unmodifiableMap(new HashMap(leaderboardsByName)); - } - - @Override - public SailingServerConfiguration getSailingServerConfiguration() { - return sailingServerConfiguration; - } - - @Override - public void updateServerConfiguration(SailingServerConfiguration serverConfiguration) { - this.sailingServerConfiguration = serverConfiguration; - mongoObjectFactory.storeServerConfiguration(serverConfiguration); - } - - @Override - public Map, Exception>> getPublicEventsOfAllSailingServers() { - return remoteSailingServerSet.getCachedEventsForRemoteSailingServers(); // FIXME should probably add our own - // stuff here... Is it enough to pass on - // the remote reference URL to the - // client for leaderboard group URL - // construction? - } - - @Override - public RemoteSailingServerReference addRemoteSailingServerReference(String name, URL url) { - RemoteSailingServerReference result = new RemoteSailingServerReferenceImpl(name, url); - remoteSailingServerSet.add(result); - mongoObjectFactory.storeSailingServer(result); - return result; - } - - @Override - public Iterable getLiveRemoteServerReferences() { - return remoteSailingServerSet.getLiveRemoteServerReferences(); - } - - @Override - public RemoteSailingServerReference getRemoteServerReferenceByName(String remoteServerReferenceName) { - return remoteSailingServerSet.getServerReferenceByName(remoteServerReferenceName); - } - - @Override - public com.sap.sse.common.Util.Pair, Exception> updateRemoteServerEventCacheSynchronously( - RemoteSailingServerReference ref) { - return remoteSailingServerSet.getEventsOrException(ref); - } - - @Override - public void removeRemoteSailingServerReference(String name) { - remoteSailingServerSet.remove(name); - mongoObjectFactory.removeSailingServer(name); - } - - @Override - public Iterable getAllEvents() { - return Collections.unmodifiableCollection(new ArrayList(eventsById.values())); - } - - @Override - public Event getEvent(Serializable id) { - return id == null ? null : eventsById.get(id); - } - - @Override - public Iterable getAllRegattas() { - return Collections.unmodifiableCollection(new ArrayList(regattasByName.values())); - } - - @Override - public boolean isRaceBeingTracked(Regatta regattaContext, RaceDefinition r) { - Set trackers = raceTrackersByRegatta.get(regattaContext); - if (trackers != null) { - for (RaceTracker tracker : trackers) { - final Set races = tracker.getRaces(); - if (races != null && races.contains(r)) { - return true; - } - } - } - return false; - } - - @Override - public Regatta getRegattaByName(String name) { - return name == null ? null : regattasByName.get(name); - } - - @Override - public Regatta getOrCreateDefaultRegatta(String name, String boatClassName, Serializable id) { - Regatta result = regattasByName.get(name); - if (result == null) { - result = new RegattaImpl(getRaceLogStore(), getRegattaLogStore(), name, getBaseDomainFactory() - .getOrCreateBoatClass(boatClassName), /* startDate */null, /* endDate */null, this, - getBaseDomainFactory().createScoringScheme(ScoringSchemeType.LOW_POINT), id, null); - logger.info("Created default regatta " + result.getName() + " (" + hashCode() + ") on " + this); - onRegattaLikeAdded(result); - cacheAndReplicateDefaultRegatta(result); - } - return result; - } - - private void onRegattaLikeAdded(IsRegattaLike isRegattaLike) { - isRegattaLike.addListener(regattaLogReplicator); - } - - private void onRegattaLikeRemoved(IsRegattaLike isRegattaLike) { - isRegattaLike.removeListener(regattaLogReplicator); - getRegattaLogStore().removeRegattaLog(isRegattaLike.getRegattaLikeIdentifier()); - } - - @Override - public Regatta createRegatta(String fullRegattaName, String boatClassName, TimePoint startDate, TimePoint endDate, - Serializable id, Iterable series, boolean persistent, ScoringScheme scoringScheme, - Serializable defaultCourseAreaId, boolean useStartTimeInference, RankingMetricConstructor rankingMetricConstructor) { - com.sap.sse.common.Util.Pair regattaWithCreatedFlag = getOrCreateRegattaWithoutReplication( - fullRegattaName, boatClassName, startDate, endDate, id, series, persistent, scoringScheme, - defaultCourseAreaId, useStartTimeInference, rankingMetricConstructor); - Regatta regatta = regattaWithCreatedFlag.getA(); - if (regattaWithCreatedFlag.getB()) { - onRegattaLikeAdded(regatta); - replicateSpecificRegattaWithoutRaceColumns(regatta); - } - return regatta; - } - - @Override - public void addRegattaWithoutReplication(Regatta regatta) { - UUID defaultCourseAreaId = null; - if (regatta.getDefaultCourseArea() != null) { - defaultCourseAreaId = regatta.getDefaultCourseArea().getId(); - } - boolean wasAdded = addAndConnectRegatta(regatta.isPersistent(), defaultCourseAreaId, regatta); - if (!wasAdded) { - logger.info("Regatta with name " + regatta.getName() + " already existed, so it hasn't been added."); - } - } - - private RaceLogStore getRaceLogStore() { - return MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(mongoObjectFactory, domainObjectFactory); - } - - private RegattaLogStore getRegattaLogStore() { - return MongoRegattaLogStoreFactory.INSTANCE.getMongoRegattaLogStore(mongoObjectFactory, domainObjectFactory); - } - - @Override - public com.sap.sse.common.Util.Pair getOrCreateRegattaWithoutReplication(String fullRegattaName, - String boatClassName, TimePoint startDate, TimePoint endDate, Serializable id, - Iterable series, boolean persistent, ScoringScheme scoringScheme, - Serializable defaultCourseAreaId, boolean useStartTimeInference, RankingMetricConstructor rankingMetricConstructor) { - CourseArea courseArea = getCourseArea(defaultCourseAreaId); - Regatta regatta = new RegattaImpl(getRaceLogStore(), getRegattaLogStore(), fullRegattaName, - getBaseDomainFactory().getOrCreateBoatClass(boatClassName), startDate, endDate, series, persistent, - scoringScheme, id, courseArea, useStartTimeInference, rankingMetricConstructor); - boolean wasCreated = addAndConnectRegatta(persistent, defaultCourseAreaId, regatta); - if (wasCreated) { - logger.info("Created regatta " + regatta.getName() + " (" + hashCode() + ") on " + this); - } - return new com.sap.sse.common.Util.Pair(regatta, wasCreated); - } - - private boolean addAndConnectRegatta(boolean persistent, Serializable defaultCourseAreaId, Regatta regatta) { - boolean wasCreated = false; - // try a quick read protected by the concurrent hash map implementation - if (!regattasByName.containsKey(regatta.getName())) { - LockUtil.lockForWrite(regattasByNameLock); - try { - // check again, now that we hold the exclusive write lock - if (!regattasByName.containsKey(regatta.getName())) { - wasCreated = true; - logger.info("putting regatta " + regatta.getName() + " (" + regatta.hashCode() - + ") into regattasByName of " + this); - regattasByName.put(regatta.getName(), regatta); - regatta.addRegattaListener(this); - regatta.addRaceColumnListener(raceLogReplicator); - regatta.addRaceColumnListener(raceLogScoringReplicator); - } - } finally { - LockUtil.unlockAfterWrite(regattasByNameLock); - } - } - if (persistent) { - updateStoredRegatta(regatta); - } - for (Event event : getAllEvents()) { - for (CourseArea eventCourseArea : event.getVenue().getCourseAreas()) { - if (defaultCourseAreaId != null && eventCourseArea.getId().equals(defaultCourseAreaId)) { - event.addRegatta(regatta); - } - } - } - return wasCreated; - } - - @Override - public void addRace(RegattaIdentifier addToRegatta, RaceDefinition raceDefinition) { - Regatta regatta = getRegatta(addToRegatta); - regatta.addRace(raceDefinition); // will trigger the raceAdded operation because this service is listening on - // all its regattas - } - - /** - * If the regatta {@link Regatta#isPersistent() is a persistent one}, the association of the race with - * the regatta is remembered persistently so that {@link #getRememberedRegattaForRace(Serializable)} will provide - * it. - */ - @Override - public void raceAdded(Regatta regatta, RaceDefinition raceDefinition) { - if (regatta.isPersistent()) { - setRegattaForRace(regatta, raceDefinition); - } - final CourseChangeReplicator listener = new CourseChangeReplicator(this, regatta, raceDefinition); - courseListeners.put(raceDefinition, listener); - raceDefinition.getCourse().addCourseListener(listener); - replicate(new AddRaceDefinition(regatta.getRegattaIdentifier(), raceDefinition)); - } - - @Override - public void raceRemoved(Regatta regatta, RaceDefinition raceDefinition) { - raceDefinition.getCourse().removeCourseListener(courseListeners.remove(raceDefinition)); - } - - private NamedReentrantReadWriteLock lockRaceTrackersById(Object trackerId) { - NamedReentrantReadWriteLock lock; - synchronized (raceTrackersByIDLocks) { - lock = raceTrackersByIDLocks.get(trackerId); - if (lock == null) { - lock = new NamedReentrantReadWriteLock("raceTrackersByIDLock for " + trackerId, /* fair */false); - raceTrackersByIDLocks.put(trackerId, lock); - } - } - LockUtil.lockForWrite(lock); - return lock; - } - - /** - * @param lock - * need to pass the lock obtained from {@link #lockRaceTrackersById(Object)} because a competing thread - * may already have removed the lock from the {@link #raceTrackersByIDLocks} map - */ - private void unlockRaceTrackersById(Object trackerId, NamedReentrantReadWriteLock lock) { - LockUtil.unlockAfterWrite(lock); - synchronized (raceTrackersByIDLocks) { - raceTrackersByIDLocks.remove(trackerId); - } - } - - @Override - public RaceHandle addRace(RegattaIdentifier regattaToAddTo, RaceTrackingConnectivityParameters params, - long timeoutInMilliseconds) throws Exception { - final Object trackerID = params.getTrackerID(); - NamedReentrantReadWriteLock raceTrackersByIdLock = lockRaceTrackersById(trackerID); - try { - RaceTracker tracker = raceTrackersByID.get(trackerID); - if (tracker == null) { - Regatta regatta = regattaToAddTo == null ? null : getRegatta(regattaToAddTo); - if (regatta == null) { - // create tracker and use an existing or create a default regatta - tracker = params.createRaceTracker(this, windStore, gpsFixStore, /* raceLogResolver */ this); - } else { - // use the regatta selected by the RaceIdentifier regattaToAddTo - tracker = params.createRaceTracker(regatta, this, windStore, gpsFixStore, /* raceLogResolver */ this); - assert tracker.getRegatta() == regatta; - } - LockUtil.lockForWrite(raceTrackersByRegattaLock); - try { - raceTrackersByID.put(trackerID, tracker); - Set trackers = raceTrackersByRegatta.get(tracker.getRegatta()); - if (trackers == null) { - trackers = Collections.newSetFromMap(new ConcurrentHashMap()); - raceTrackersByRegatta.put(tracker.getRegatta(), trackers); - } - trackers.add(tracker); - } finally { - LockUtil.unlockAfterWrite(raceTrackersByRegattaLock); - } - // TODO we assume here that the regatta name is unique which necessitates adding the boat class name to - // it in RegattaImpl constructor - String regattaName = tracker.getRegatta().getName(); - Regatta regattaWithName = regattasByName.get(regattaName); - // TODO we assume here that the regatta name is unique which necessitates adding the boat class name to - // it in RegattaImpl constructor - if (regattaWithName != null) { - if (regattaWithName != tracker.getRegatta()) { - if (Util.isEmpty(regattaWithName.getAllRaces())) { - // probably, tracker removed the last races from the old regatta and created a new one - LockUtil.lockForWrite(regattasByNameLock); - try { - regattasByName.remove(regattaName); - cacheAndReplicateDefaultRegatta(tracker.getRegatta()); - } finally { - LockUtil.unlockAfterWrite(regattasByNameLock); - } - } else { - throw new RuntimeException("Internal error. Two regatta objects with equal name " - + regattaName); - } - } - } else { - cacheAndReplicateDefaultRegatta(tracker.getRegatta()); - } - } else { - logger.warning("Race tracker with ID "+trackerID+" already found; not tracking twice to avoid race duplication"); - WindStore existingTrackersWindStore = tracker.getWindStore(); - if (!existingTrackersWindStore.equals(windStore)) { - logger.warning("Wind store mismatch. Requested wind store: " + windStore - + ". Wind store in use by existing tracker: " + existingTrackersWindStore); - } - GPSFixStore existingTrackersGPSFixStore = tracker.getGPSFixStore(); - if (!existingTrackersGPSFixStore.equals(gpsFixStore)) { - logger.warning("GPSFix store mismatch. Requested GPSFix store: " + gpsFixStore - + ". GPSFix store in use by existing tracker: " + existingTrackersGPSFixStore); - } - } - if (timeoutInMilliseconds != -1) { - scheduleAbortTrackerAfterInitialTimeout(tracker, timeoutInMilliseconds); - } - return tracker.getRacesHandle(); - } finally { - unlockRaceTrackersById(trackerID, raceTrackersByIdLock); - } - } - - /** - * The regatta and all its contained {@link Regatta#getAllRaces() races} are replicated to all replicas. - * - * @param regatta - * the series of this regatta must not have any {@link Series#getRaceColumns() race columns associated - * (yet)}. - */ - private void replicateSpecificRegattaWithoutRaceColumns(Regatta regatta) { - Serializable courseAreaId = null; - if (regatta.getDefaultCourseArea() != null) { - courseAreaId = regatta.getDefaultCourseArea().getId(); - } - replicate(new AddSpecificRegatta(regatta.getName(), regatta.getBoatClass() == null ? null : regatta - .getBoatClass().getName(), regatta.getStartDate(), regatta.getEndDate(), regatta.getId(), - getSeriesWithoutRaceColumnsConstructionParametersAsMap(regatta), regatta.isPersistent(), - regatta.getScoringScheme(), courseAreaId, regatta.useStartTimeInference(), regatta.getRankingMetricType())); - RegattaIdentifier regattaIdentifier = regatta.getRegattaIdentifier(); - for (RaceDefinition race : regatta.getAllRaces()) { - replicate(new AddRaceDefinition(regattaIdentifier, race)); - } - } - - private RegattaCreationParametersDTO getSeriesWithoutRaceColumnsConstructionParametersAsMap(Regatta regatta) { - LinkedHashMap result = new LinkedHashMap(); - for (Series s : regatta.getSeries()) { - assert Util.isEmpty(s.getRaceColumns()); - List fleetNamesAndOrdering = new ArrayList(); - for (Fleet f : s.getFleets()) { - fleetNamesAndOrdering.add(getBaseDomainFactory().convertToFleetDTO(f)); - } - result.put( - s.getName(), - new SeriesCreationParametersDTO(fleetNamesAndOrdering, s.isMedal(), s.isStartsWithZeroScore(), s - .isFirstColumnIsNonDiscardableCarryForward(), s.getResultDiscardingRule() == null ? null - : s.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces(), s - .hasSplitFleetContiguousScoring())); - } - return new RegattaCreationParametersDTO(result); - } - - /** - * If regatta is not yet in {@link #regattasByName}, it is added, this service is - * {@link Regatta#addRegattaListener(RegattaListener) added} as regatta listener, and the regatta and all its - * contained {@link Regatta#getAllRaces() races} are replicated to all replica. - */ - private void cacheAndReplicateDefaultRegatta(Regatta regatta) { - // try a quick read first, protected by regattasByName being a concurrent hash set - if (!regattasByName.containsKey(regatta.getName())) { - // now we need to obtain exclusive write access; in between, some other thread may have added a regatta by - // that - // name, so we need to check again: - LockUtil.lockForWrite(regattasByNameLock); - try { - if (!regattasByName.containsKey(regatta.getName())) { - logger.info("putting regatta " + regatta.getName() + " (" + regatta.hashCode() - + ") into regattasByName of " + this); - regattasByName.put(regatta.getName(), regatta); - regatta.addRegattaListener(this); - regatta.addRaceColumnListener(raceLogReplicator); - regatta.addRaceColumnListener(raceLogScoringReplicator); - - replicate(new AddDefaultRegatta(regatta.getName(), regatta.getBoatClass() == null ? null : regatta - .getBoatClass().getName(), regatta.getStartDate(), regatta.getEndDate(), regatta.getId())); - RegattaIdentifier regattaIdentifier = regatta.getRegattaIdentifier(); - for (RaceDefinition race : regatta.getAllRaces()) { - replicate(new AddRaceDefinition(regattaIdentifier, race)); - } - } - } finally { - LockUtil.unlockAfterWrite(regattasByNameLock); - } - } - } - - @Override - public DynamicTrackedRace createTrackedRace(RegattaAndRaceIdentifier raceIdentifier, WindStore windStore, - GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, - long millisecondsOverWhichToAverageSpeed, boolean useMarkPassingCalculator) { - DynamicTrackedRegatta trackedRegatta = getOrCreateTrackedRegatta(getRegatta(raceIdentifier)); - RaceDefinition race = getRace(raceIdentifier); - return trackedRegatta.createTrackedRace(race, Collections. emptyList(), windStore, gpsFixStore, - delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed, - /* raceDefinitionSetToUpdate */null, useMarkPassingCalculator, /* raceLogResolver */ this); - } - - private void ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking( - DynamicTrackedRegatta trackedRegatta) { - if (regattasObservedForDefaultLeaderboard.add(trackedRegatta)) { - trackedRegatta.addRaceListener(new RaceAdditionListener()); - } - } - - private void stopObservingRegattaForRedaultLeaderboardAndAutoLeaderboardLinking(DynamicTrackedRegatta trackedRegatta) { - regattasObservedForDefaultLeaderboard.remove(trackedRegatta); - } - - /** - * A listener class used to ensure that when a tracked race is added to any {@link TrackedRegatta} managed by this - * service, the service adds the tracked race to the default leaderboard and links it to the leaderboard columns - * that were previously connected to it. Additionally, a {@link RaceChangeListener} is added to the - * {@link TrackedRace} which is responsible for triggering the replication of all relevant changes to the tracked - * race. When a tracked race is removed, the {@link TrackedRaceReplicator} that was added as listener to that - * tracked race is removed again. - * - * A {@link PolarFixCacheUpdater} is added to every race so that polar fixes are aggregated when new GPS fixes - * arrive. - * - * @author Axel Uhl (d043530) - * - */ - private class RaceAdditionListener implements RaceListener, Serializable { - private static final long serialVersionUID = 1036955460477000265L; - - private final Map trackedRaceReplicators; - - private final Map polarFixCacheUpdaters; - - public RaceAdditionListener() { - this.trackedRaceReplicators = new HashMap(); - this.polarFixCacheUpdaters = new HashMap(); - } - - @Override - public void raceRemoved(TrackedRace trackedRace) { - TrackedRaceReplicator trackedRaceReplicator = trackedRaceReplicators.remove(trackedRace); - if (trackedRaceReplicator != null) { - trackedRace.removeListener(trackedRaceReplicator); - } - PolarFixCacheUpdater polarFixCacheUpdater = polarFixCacheUpdaters.remove(trackedRace); - if (polarFixCacheUpdater != null) { - trackedRace.removeListener(polarFixCacheUpdater); - } - } - - @Override - public void raceAdded(TrackedRace trackedRace) { - // replicate the addition of the tracked race: - CreateTrackedRace op = new CreateTrackedRace(trackedRace.getRaceIdentifier(), trackedRace.getWindStore(), - trackedRace.getGPSFixStore(), trackedRace.getDelayToLiveInMillis(), - trackedRace.getMillisecondsOverWhichToAverageWind(), - trackedRace.getMillisecondsOverWhichToAverageSpeed()); - replicate(op); - linkRaceToConfiguredLeaderboardColumns(trackedRace); - TrackedRaceReplicator trackedRaceReplicator = new TrackedRaceReplicator(trackedRace); - trackedRaceReplicators.put(trackedRace, trackedRaceReplicator); - trackedRace.addListener(trackedRaceReplicator, /* fire wind already loaded */true, true); - - PolarFixCacheUpdater polarFixCacheUpdater = new PolarFixCacheUpdater(trackedRace); - polarFixCacheUpdaters.put(trackedRace, polarFixCacheUpdater); - trackedRace.addListener(polarFixCacheUpdater); - - if (polarDataService != null) { - trackedRace.setPolarDataService(polarDataService); - } - } - } - - private class PolarFixCacheUpdater extends AbstractRaceChangeListener { - - private final TrackedRace race; - - public PolarFixCacheUpdater(TrackedRace race) { - this.race = race; - } - - @Override - public void competitorPositionChanged(GPSFixMoving fix, Competitor item) { - if (polarDataService != null) { - polarDataService.competitorPositionChanged(fix, item, race); - } - } - - @Override - public void statusChanged(TrackedRaceStatus newStatus, TrackedRaceStatus oldStatus) { - if (oldStatus.getStatus() == TrackedRaceStatusEnum.LOADING - && newStatus.getStatus() != TrackedRaceStatusEnum.LOADING) { - if (polarDataService != null) { - polarDataService.raceFinishedLoading(race); - } - } - } - - } - - private class TrackedRaceReplicator implements RaceChangeListener { - private final TrackedRace trackedRace; - - public TrackedRaceReplicator(TrackedRace trackedRace) { - this.trackedRace = trackedRace; - } - - @Override - public void windSourcesToExcludeChanged(Iterable windSourcesToExclude) { - replicate(new UpdateWindSourcesToExclude(getRaceIdentifier(), windSourcesToExclude)); - } - - @Override - public void startOfTrackingChanged(TimePoint startOfTracking) { - replicate(new UpdateStartOfTracking(getRaceIdentifier(), startOfTracking)); - } - - @Override - public void endOfTrackingChanged(TimePoint endOfTracking) { - replicate(new UpdateEndOfTracking(getRaceIdentifier(), endOfTracking)); - } - - @Override - public void startTimeReceivedChanged(TimePoint startTimeReceived) { - replicate(new UpdateStartTimeReceived(getRaceIdentifier(), startTimeReceived)); - } - - @Override - public void startOfRaceChanged(TimePoint oldStartOfRace, TimePoint newStartOfRace) { - // no action required; the update signaled by this call is implicit; for explicit updates - // see raceTimesChanged(TimePoint, TimePoint, TimePoint). - } - - @Override - public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) { - // no-op; the course change is replicated by the separate CourseChangeReplicator - } - - @Override - public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) { - // no-op; the course change is replicated by the separate CourseChangeReplicator - } - - @Override - public void delayToLiveChanged(long delayToLiveInMillis) { - replicate(new UpdateRaceDelayToLive(getRaceIdentifier(), delayToLiveInMillis)); - } - - @Override - public void windDataReceived(Wind wind, WindSource windSource) { - replicate(new RecordWindFix(getRaceIdentifier(), windSource, wind)); - } - - @Override - public void windDataRemoved(Wind wind, WindSource windSource) { - replicate(new RemoveWindFix(getRaceIdentifier(), windSource, wind)); - } - - @Override - public void windAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { - replicate(new UpdateWindAveragingTime(getRaceIdentifier(), newMillisecondsOverWhichToAverage)); - } - - @Override - public void competitorPositionChanged(GPSFixMoving fix, Competitor competitor) { - replicate(new RecordCompetitorGPSFix(getRaceIdentifier(), competitor, fix)); - } - - @Override - public void statusChanged(TrackedRaceStatus newStatus, TrackedRaceStatus oldStatus) { - replicate(new UpdateTrackedRaceStatus(getRaceIdentifier(), newStatus)); - } - - @Override - public void markPositionChanged(GPSFix fix, Mark mark, boolean firstInTrack) { - final RecordMarkGPSFix operation; - if (firstInTrack) { - operation = new RecordMarkGPSFixForNewMarkTrack(getRaceIdentifier(), mark, fix); - } else { - operation = new RecordMarkGPSFixForExistingTrack(getRaceIdentifier(), mark, fix); - } - replicate(operation); - } - - @Override - public void markPassingReceived(Competitor competitor, Map oldMarkPassings, - Iterable markPassings) { - replicate(new UpdateMarkPassings(getRaceIdentifier(), competitor, markPassings)); - } - - @Override - public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { - replicate(new UpdateWindAveragingTime(getRaceIdentifier(), newMillisecondsOverWhichToAverage)); - } - - private RegattaAndRaceIdentifier getRaceIdentifier() { - return trackedRace.getRaceIdentifier(); - } - - } - - /** - * Based on the trackedRace's {@link TrackedRace#getRaceIdentifier() race identifier}, the tracked race - * is (re-)associated to all {@link RaceColumn race columns} that currently have no - * {@link RaceColumn#getTrackedRace(Fleet) tracked race assigned} and whose - * {@link RaceColumn#getRaceIdentifier(Fleet) race identifier} equals that of trackedRace. - */ - private void linkRaceToConfiguredLeaderboardColumns(TrackedRace trackedRace) { - boolean leaderboardHasChanged = false; - RegattaAndRaceIdentifier trackedRaceIdentifier = trackedRace.getRaceIdentifier(); - for (Leaderboard leaderboard : getLeaderboards().values()) { - for (RaceColumn column : leaderboard.getRaceColumns()) { - for (Fleet fleet : column.getFleets()) { - if (trackedRaceIdentifier.equals(column.getRaceIdentifier(fleet)) - && column.getTrackedRace(fleet) == null) { - column.setTrackedRace(fleet, trackedRace); - leaderboardHasChanged = true; - replicate(new ConnectTrackedRaceToLeaderboardColumn(leaderboard.getName(), column.getName(), - fleet.getName(), trackedRaceIdentifier)); - } - } - } - if (leaderboardHasChanged) { - // Update the corresponding groups, to keep them in sync - syncGroupsAfterLeaderboardChange(leaderboard, /* doDatabaseUpdate */false); - } - } - } - - @Override - public void stopTracking(Regatta regatta) throws MalformedURLException, IOException, InterruptedException { - final Set trackersForRegatta = raceTrackersByRegatta.get(regatta); - if (trackersForRegatta != null) { - for (RaceTracker raceTracker : trackersForRegatta) { - final Set races = raceTracker.getRaces(); - if (races != null) { - for (RaceDefinition race : races) { - stopTrackingWind(regatta, race); - } - } - raceTracker.stop(/* preemptive */false); - final Object trackerId = raceTracker.getID(); - final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId); - try { - raceTrackersByID.remove(trackerId); - } finally { - unlockRaceTrackersById(trackerId, lock); - } - raceTrackersByID.remove(trackerId); - } - LockUtil.lockForWrite(raceTrackersByRegattaLock); - try { - raceTrackersByRegatta.remove(regatta); - } finally { - LockUtil.unlockAfterWrite(raceTrackersByRegattaLock); - } - } - } - - @Override - public void stopTrackingAndRemove(Regatta regatta) throws MalformedURLException, IOException, InterruptedException { - stopTracking(regatta); - if (regatta != null) { - if (regatta.getName() != null) { - logger.info("Removing regatta " + regatta.getName() + " (" + regatta.hashCode() + ") from " + this); - LockUtil.lockForWrite(regattasByNameLock); - try { - regattasByName.remove(regatta.getName()); - } finally { - LockUtil.unlockAfterWrite(regattasByNameLock); - } - LockUtil.lockForWrite(regattaTrackingCacheLock); - try { - regattaTrackingCache.remove(regatta); - } finally { - LockUtil.unlockAfterWrite(regattaTrackingCacheLock); - } - regatta.removeRegattaListener(this); - regatta.removeRaceColumnListener(raceLogReplicator); - regatta.removeRaceColumnListener(raceLogScoringReplicator); - } - for (RaceDefinition race : regatta.getAllRaces()) { - stopTrackingWind(regatta, race); - } - } - } - - /** - * The tracker will initially try to connect to the tracking infrastructure to obtain basic race master data. If - * this fails after some timeout, to avoid garbage and lingering threads, the task scheduled by this method will - * check after the timeout expires if race master data was successfully received. If so, the tracker continues - * normally. Otherwise, the tracker is shut down orderly by calling {@link RaceTracker#stop(boolean) stopping}. - * - * @return the scheduled task, in case the caller wants to {@link ScheduledFuture#cancel(boolean) cancel} it, e.g., - * when the tracker is stopped or has successfully received the race - */ - private ScheduledFuture scheduleAbortTrackerAfterInitialTimeout(final RaceTracker tracker, - final long timeoutInMilliseconds) { - ScheduledFuture task = getScheduler().schedule(new Runnable() { - @Override - public void run() { - if (tracker.getRaces() == null || tracker.getRaces().isEmpty()) { - try { - Regatta regatta = tracker.getRegatta(); - logger.log(Level.SEVERE, "RaceDefinition for a race in regatta " + regatta.getName() - + " not obtained within " + timeoutInMilliseconds - + "ms. Aborting tracker for this race."); - Set trackersForRegatta = raceTrackersByRegatta.get(regatta); - if (trackersForRegatta != null) { - trackersForRegatta.remove(tracker); - } - tracker.stop(/* preemptive */true); - final Object trackerId = tracker.getID(); - final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId); - try { - raceTrackersByID.remove(trackerId); - } finally { - unlockRaceTrackersById(trackerId, lock); - } - if (trackersForRegatta == null || trackersForRegatta.isEmpty()) { - stopTracking(regatta); - } - } catch (Exception e) { - logger.log(Level.SEVERE, "scheduleAbortTrackerAfterInitialTimeout", e); - e.printStackTrace(); - } - } - } - }, /* delay */timeoutInMilliseconds, /* unit */TimeUnit.MILLISECONDS); - return task; - } - - @Override - public void stopTracking(Regatta regatta, RaceDefinition race) throws MalformedURLException, IOException, - InterruptedException { - logger.info("Stopping tracking for " + race + "..."); - final Set trackerSet = raceTrackersByRegatta.get(regatta); - if (trackerSet != null) { - Iterator trackerIter = trackerSet.iterator(); - while (trackerIter.hasNext()) { - RaceTracker raceTracker = trackerIter.next(); - if (raceTracker.getRaces() != null && raceTracker.getRaces().contains(race)) { - logger.info("Found tracker to stop for races " + raceTracker.getRaces()); - raceTracker.stop(/* preemptive */false); - trackerIter.remove(); - final Object trackerId = raceTracker.getID(); - final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId); - try { - raceTrackersByID.remove(trackerId); - } finally { - unlockRaceTrackersById(trackerId, lock); - } - } - } - } else { - logger.warning("Didn't find any trackers for regatta " + regatta); - } - stopTrackingWind(regatta, race); - // if the last tracked race was removed, confirm that tracking for the entire regatta has stopped - if (trackerSet == null || trackerSet.isEmpty()) { - stopTracking(regatta); - } - } - - @Override - public void removeRegatta(Regatta regatta) throws MalformedURLException, IOException, InterruptedException { - Set leaderboardsToRemove = new HashSet<>(); - for (Leaderboard leaderboard : getLeaderboards().values()) { - if (leaderboard instanceof RegattaLeaderboard) { - RegattaLeaderboard regattaLeaderboard = (RegattaLeaderboard) leaderboard; - if (regattaLeaderboard.getRegatta() == regatta) { - leaderboardsToRemove.add(regattaLeaderboard); - } - } - } - for (RegattaLeaderboard regattaLeaderboardToRemove : leaderboardsToRemove) { - removeLeaderboard(regattaLeaderboardToRemove.getName()); - } - // avoid ConcurrentModificationException by copying the races to remove: - Set racesToRemove = new HashSet<>(); - Util.addAll(regatta.getAllRaces(), racesToRemove); - for (RaceDefinition race : racesToRemove) { - removeRace(regatta, race); - mongoObjectFactory.removeRegattaForRaceID(race.getName(), regatta); - persistentRegattasForRaceIDs.remove(race.getId().toString()); - } - if (regatta.isPersistent()) { - mongoObjectFactory.removeRegatta(regatta); - } - LockUtil.lockForWrite(regattasByNameLock); - try { - regattasByName.remove(regatta.getName()); - } finally { - LockUtil.unlockAfterWrite(regattasByNameLock); - } - regatta.removeRegattaListener(this); - regatta.removeRaceColumnListener(raceLogReplicator); - regatta.removeRaceColumnListener(raceLogScoringReplicator); - onRegattaLikeRemoved(regatta); - } - - @Override - public void removeSeries(Series series) throws MalformedURLException, IOException, InterruptedException { - Regatta regatta = series.getRegatta(); - regatta.removeSeries(series); - if (regatta.isPersistent()) { - mongoObjectFactory.storeRegatta(regatta); - } - } - - @Override - public Regatta updateRegatta(RegattaIdentifier regattaIdentifier, TimePoint startDate, TimePoint endDate, - Serializable newDefaultCourseAreaId, RegattaConfiguration newRegattaConfiguration, - Iterable series, boolean useStartTimeInference) { - // We're not doing any renaming of the regatta itself, therefore we don't have to sync on the maps. - Regatta regatta = getRegatta(regattaIdentifier); - CourseArea newCourseArea = getCourseArea(newDefaultCourseAreaId); - if (newCourseArea != regatta.getDefaultCourseArea()) { - regatta.setDefaultCourseArea(newCourseArea); - } - regatta.setStartDate(startDate); - regatta.setEndDate(endDate); - if (regatta.useStartTimeInference() != useStartTimeInference) { - regatta.setUseStartTimeInference(useStartTimeInference); - final DynamicTrackedRegatta trackedRegatta = getTrackedRegatta(regatta); - if (trackedRegatta != null) { - trackedRegatta.lockTrackedRacesForRead(); - try { - for (DynamicTrackedRace trackedRace : trackedRegatta.getTrackedRaces()) { - // the start times of the regatta's tracked races now have to be re-evaluated the next time they - // are queried - trackedRace.invalidateStartTime(); - } - } finally { - trackedRegatta.unlockTrackedRacesAfterRead(); - } - } - } - regatta.setRegattaConfiguration(newRegattaConfiguration); - if (series != null) { - for (Series seriesObj : series) { - regatta.addSeries(seriesObj); - } - } - regatta.adjustEventToRegattaAssociation(this); - if (regatta.isPersistent()) { - mongoObjectFactory.storeRegatta(regatta); - } - return regatta; - } - - @Override - public void removeRace(Regatta regatta, RaceDefinition race) throws MalformedURLException, IOException, - InterruptedException { - logger.info("Removing the race " + race + "..."); - stopAllTrackersForWhichRaceIsLastReachable(regatta, race); - stopTrackingWind(regatta, race); - TrackedRace trackedRace = getExistingTrackedRace(regatta, race); - if (trackedRace != null) { - TrackedRegatta trackedRegatta = getTrackedRegatta(regatta); - final boolean isTrackedRacesEmpty; - if (trackedRegatta != null) { - trackedRegatta.lockTrackedRacesForWrite(); - try { - trackedRegatta.removeTrackedRace(trackedRace); - isTrackedRacesEmpty = Util.isEmpty(trackedRegatta.getTrackedRaces()); - } finally { - trackedRegatta.unlockTrackedRacesAfterWrite(); - } - } else { - isTrackedRacesEmpty = false; - } - if (isTrackedRacesEmpty) { - removeTrackedRegatta(regatta); - } - // remove tracked race from RaceColumns of regatta - for (Series series : regatta.getSeries()) { - for (RaceColumnInSeries raceColumn : series.getRaceColumns()) { - for (Fleet fleet : series.getFleets()) { - if (raceColumn.getTrackedRace(fleet) == trackedRace) { - raceColumn.releaseTrackedRace(fleet); - } - } - } - } - for (Leaderboard leaderboard : getLeaderboards().values()) { - if (leaderboard instanceof FlexibleLeaderboard) { // RegattaLeaderboards have implicitly been updated by - // the code above - for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { - for (Fleet fleet : raceColumn.getFleets()) { - if (raceColumn.getTrackedRace(fleet) == trackedRace) { - raceColumn.releaseTrackedRace(fleet); // but leave the RaceIdentifier on the race column - // untouched, e.g., for later re-load - } - } - } - } - } - } - // remove the race from the (default) regatta if the regatta is not persistently stored - regatta.removeRace(race); - if (!regatta.isPersistent() && Util.isEmpty(regatta.getAllRaces())) { - logger.info("Removing regatta " + regatta.getName() + " (" + regatta.hashCode() + ") from service " + this); - LockUtil.lockForWrite(regattasByNameLock); - try { - regattasByName.remove(regatta.getName()); - } finally { - LockUtil.unlockAfterWrite(regattasByNameLock); - } - regatta.removeRegattaListener(this); - regatta.removeRaceColumnListener(raceLogReplicator); - regatta.removeRaceColumnListener(raceLogScoringReplicator); - } - } - - /** - * Doesn't stop any wind trackers - */ - private void stopAllTrackersForWhichRaceIsLastReachable(Regatta regatta, RaceDefinition race) - throws MalformedURLException, IOException, InterruptedException { - if (raceTrackersByRegatta.containsKey(regatta)) { - Iterator trackerIter = raceTrackersByRegatta.get(regatta).iterator(); - while (trackerIter.hasNext()) { - RaceTracker raceTracker = trackerIter.next(); - if (raceTracker.getRaces() != null && raceTracker.getRaces().contains(race)) { - boolean foundReachableRace = false; - for (RaceDefinition raceTrackedByTracker : raceTracker.getRaces()) { - if (raceTrackedByTracker != race && isReachable(regatta, raceTrackedByTracker)) { - foundReachableRace = true; - break; - } - } - if (!foundReachableRace) { - // firstly stop the tracker - raceTracker.stop(/* preemptive */true); - // remove it from the raceTrackers by Regatta - trackerIter.remove(); - final Object trackerId = raceTracker.getID(); - final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId); - try { - raceTrackersByID.remove(trackerId); - } finally { - unlockRaceTrackersById(trackerId, lock); - } - // if the last tracked race was removed, remove the entire regatta - if (raceTrackersByRegatta.get(regatta).isEmpty()) { - stopTracking(regatta); - } - } - } - } - } - } - - private boolean isReachable(Regatta regatta, RaceDefinition race) { - return Util.contains(regatta.getAllRaces(), race); - } - - @Override - public void startTrackingWind(Regatta regatta, RaceDefinition race, boolean correctByDeclination) throws Exception { - for (WindTrackerFactory windTrackerFactory : getWindTrackerFactories()) { - windTrackerFactory.createWindTracker(getOrCreateTrackedRegatta(regatta), race, correctByDeclination); - } - } - - @Override - public void stopTrackingWind(Regatta regatta, RaceDefinition race) throws SocketException, IOException { - for (WindTrackerFactory windTrackerFactory : getWindTrackerFactories()) { - WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race); - if (windTracker != null) { - windTracker.stop(); - } - } - } - - @Override - public Iterable> getWindTrackedRaces() { - List> result = new ArrayList>(); - for (Regatta regatta : getAllRegattas()) { - for (RaceDefinition race : regatta.getAllRaces()) { - for (WindTrackerFactory windTrackerFactory : getWindTrackerFactories()) { - WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race); - if (windTracker != null) { - result.add(new com.sap.sse.common.Util.Triple(regatta, race, - windTracker.toString())); - } - } - } - } - return result; - } - - @Override - public DynamicTrackedRace getTrackedRace(Regatta regatta, RaceDefinition race) { - return getOrCreateTrackedRegatta(regatta).getTrackedRace(race); - } - - private DynamicTrackedRace getExistingTrackedRace(Regatta regatta, RaceDefinition race) { - return getOrCreateTrackedRegatta(regatta).getExistingTrackedRace(race); - } - - @Override - public DynamicTrackedRegatta getOrCreateTrackedRegatta(Regatta regatta) { - cacheAndReplicateDefaultRegatta(regatta); - LockUtil.lockForWrite(regattaTrackingCacheLock); - try { - DynamicTrackedRegatta result = regattaTrackingCache.get(regatta); - if (result == null) { - logger.info("Creating DynamicTrackedRegattaImpl for regatta " + regatta.getName() + " with hashCode " - + regatta.hashCode()); - result = new DynamicTrackedRegattaImpl(regatta); - replicate(new TrackRegatta(regatta.getRegattaIdentifier())); - regattaTrackingCache.put(regatta, result); - ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(result); - } - return result; - } finally { - LockUtil.unlockAfterWrite(regattaTrackingCacheLock); - } - } - - @Override - public DynamicTrackedRegatta getTrackedRegatta(com.sap.sailing.domain.base.Regatta regatta) { - return regattaTrackingCache.get(regatta); - } - - @Override - public void removeTrackedRegatta(Regatta regatta) { - logger.info("Removing regatta " + regatta.getName() + " from regattaTrackingCache"); - final DynamicTrackedRegatta trackedRegatta; - LockUtil.lockForWrite(regattaTrackingCacheLock); - try { - trackedRegatta = regattaTrackingCache.remove(regatta); - } finally { - LockUtil.unlockAfterWrite(regattaTrackingCacheLock); - } - stopObservingRegattaForRedaultLeaderboardAndAutoLeaderboardLinking(trackedRegatta); - } - - @Override - public Regatta getRegatta(RegattaName regattaName) { - return (Regatta) regattasByName.get(regattaName.getRegattaName()); - } - - @Override - public Regatta getRegatta(RegattaIdentifier regattaIdentifier) { - return (Regatta) regattaIdentifier.getRegatta(this); - } - - @Override - public DynamicTrackedRace getTrackedRace(RegattaAndRaceIdentifier raceIdentifier) { - DynamicTrackedRace result = null; - Regatta regatta = regattasByName.get(raceIdentifier.getRegattaName()); - if (regatta != null) { - DynamicTrackedRegatta trackedRegatta = regattaTrackingCache.get(regatta); - if (trackedRegatta != null) { - RaceDefinition race = getRace(raceIdentifier); - if (race != null) { - result = trackedRegatta.getTrackedRace(race); - } - } - } - return result; - } - - @Override - public DynamicTrackedRace getExistingTrackedRace(RegattaAndRaceIdentifier raceIdentifier) { - Regatta regatta = getRegattaByName(raceIdentifier.getRegattaName()); - DynamicTrackedRace trackedRace = null; - if (regatta != null) { - RaceDefinition race = regatta.getRaceByName(raceIdentifier.getRaceName()); - trackedRace = getOrCreateTrackedRegatta(regatta).getExistingTrackedRace(race); - } - return trackedRace; - } - - @Override - public RaceDefinition getRace(RegattaAndRaceIdentifier regattaNameAndRaceName) { - RaceDefinition result = null; - Regatta regatta = getRegatta(regattaNameAndRaceName); - if (regatta != null) { - result = regatta.getRaceByName(regattaNameAndRaceName.getRaceName()); - } - return result; - } - - @Override - public Map getLeaderboardGroups() { - return Collections.unmodifiableMap(new HashMap(leaderboardGroupsByName)); - } - - @Override - public LeaderboardGroup getLeaderboardGroupByName(String groupName) { - return leaderboardGroupsByName.get(groupName); - } - - @Override - public LeaderboardGroup getLeaderboardGroupByID(UUID leaderboardGroupID) { - return leaderboardGroupsByID.get(leaderboardGroupID); - } - - @Override - public LeaderboardGroup addLeaderboardGroup(UUID id, String groupName, String description, String displayName, - boolean displayGroupsInReverseOrder, List leaderboardNames, - int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType) { - ArrayList leaderboards = new ArrayList<>(); - for (String leaderboardName : leaderboardNames) { - Leaderboard leaderboard = leaderboardsByName.get(leaderboardName); - if (leaderboard == null) { - throw new IllegalArgumentException("No leaderboard with name " + leaderboardName + " found"); - } else { - leaderboards.add(leaderboard); - } - } - LeaderboardGroup result = new LeaderboardGroupImpl(id, groupName, description, displayName, - displayGroupsInReverseOrder, leaderboards); - if (overallLeaderboardScoringSchemeType != null) { - // create overall leaderboard and its discards settings - addOverallLeaderboardToLeaderboardGroup(result, - getBaseDomainFactory().createScoringScheme(overallLeaderboardScoringSchemeType), - overallLeaderboardDiscardThresholds); - } - LockUtil.lockForWrite(leaderboardGroupsByNameLock); - try { - if (leaderboardGroupsByName.containsKey(groupName)) { - throw new IllegalArgumentException("Leaderboard group with name " + groupName + " already exists"); - } - leaderboardGroupsByName.put(groupName, result); - leaderboardGroupsByID.put(result.getId(), result); - } finally { - LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock); - } - mongoObjectFactory.storeLeaderboardGroup(result); - return result; - } - - @Override - public void addLeaderboardGroupWithoutReplication(LeaderboardGroup leaderboardGroup) { - LockUtil.lockForWrite(leaderboardGroupsByNameLock); - try { - String groupName = leaderboardGroup.getName(); - if (leaderboardGroupsByName.containsKey(groupName)) { - throw new IllegalArgumentException("Leaderboard group with name " + groupName + " already exists"); - } - leaderboardGroupsByName.put(groupName, leaderboardGroup); - leaderboardGroupsByID.put(leaderboardGroup.getId(), leaderboardGroup); - } finally { - LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock); - } - if (leaderboardGroup.hasOverallLeaderboard()) { - addLeaderboard(leaderboardGroup.getOverallLeaderboard()); - } - mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup); - } - - @Override - public void removeLeaderboardGroup(String groupName) { - final LeaderboardGroup leaderboardGroup; - LockUtil.lockForWrite(leaderboardGroupsByNameLock); - try { - leaderboardGroup = leaderboardGroupsByName.remove(groupName); - if (leaderboardGroup != null) { - leaderboardGroupsByID.remove(leaderboardGroup.getId()); - } - } finally { - LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock); - } - mongoObjectFactory.removeLeaderboardGroup(groupName); - if (leaderboardGroup != null && leaderboardGroup.getOverallLeaderboard() != null) { - removeLeaderboard(leaderboardGroup.getOverallLeaderboard().getName()); - } - } - - @Override - public void renameLeaderboardGroup(String oldName, String newName) { - LockUtil.lockForWrite(leaderboardGroupsByNameLock); - try { - final LeaderboardGroup toRename = leaderboardGroupsByName.get(oldName); - if (toRename == null) { - throw new IllegalArgumentException("No leaderboard group with name " + oldName + " found"); - } - if (leaderboardGroupsByName.containsKey(newName)) { - throw new IllegalArgumentException("Leaderboard group with name " + newName + " already exists"); - } - leaderboardGroupsByName.remove(oldName); - toRename.setName(newName); - leaderboardGroupsByName.put(newName, toRename); - } finally { - LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock); - } - mongoObjectFactory.renameLeaderboardGroup(oldName, newName); - } - - @Override - public void updateLeaderboardGroup(String oldName, String newName, String description, String displayName, - List leaderboardNames, int[] overallLeaderboardDiscardThresholds, - ScoringSchemeType overallLeaderboardScoringSchemeType) { - if (!oldName.equals(newName)) { - renameLeaderboardGroup(oldName, newName); - } - LeaderboardGroup group = getLeaderboardGroupByName(newName); - if (!description.equals(group.getDescription())) { - group.setDescriptiom(description); - } - if (!Util.equalsWithNull(displayName, group.getDisplayName())) { - group.setDisplayName(displayName); - } - group.clearLeaderboards(); - for (String leaderboardName : leaderboardNames) { - Leaderboard leaderboard = getLeaderboardByName(leaderboardName); - if (leaderboard != null) { - group.addLeaderboard(leaderboard); - } - } - Leaderboard overallLeaderboard = group.getOverallLeaderboard(); - if (overallLeaderboard != null) { - if (overallLeaderboardScoringSchemeType == null) { - group.setOverallLeaderboard(null); - removeLeaderboard(overallLeaderboard.getName()); - } else { - // update existing overall leaderboard's discards settings; scoring scheme cannot be updated in-place - overallLeaderboard.setCrossLeaderboardResultDiscardingRule(new ThresholdBasedResultDiscardingRuleImpl( - overallLeaderboardDiscardThresholds)); - updateStoredLeaderboard(overallLeaderboard); - } - } else if (overallLeaderboard == null && overallLeaderboardScoringSchemeType != null) { - addOverallLeaderboardToLeaderboardGroup(group, - getBaseDomainFactory().createScoringScheme(overallLeaderboardScoringSchemeType), - overallLeaderboardDiscardThresholds); - } - updateStoredLeaderboardGroup(group); - } - - private void addOverallLeaderboardToLeaderboardGroup(LeaderboardGroup leaderboardGroup, - ScoringScheme scoringScheme, int[] discardThresholds) { - Leaderboard overallLeaderboard = new LeaderboardGroupMetaLeaderboard(leaderboardGroup, scoringScheme, - new ThresholdBasedResultDiscardingRuleImpl(discardThresholds)); - leaderboardGroup.setOverallLeaderboard(overallLeaderboard); - addLeaderboard(overallLeaderboard); - updateStoredLeaderboard(overallLeaderboard); - } - - @Override - public void updateStoredLeaderboardGroup(LeaderboardGroup leaderboardGroup) { - mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup); - } - - private ScheduledExecutorService getScheduler() { - return scheduler; - } - - @Override - public ObjectInputStream createObjectInputStreamResolvingAgainstCache(InputStream is) throws IOException { - return getBaseDomainFactory().createObjectInputStreamResolvingAgainstThisFactory(is); - } - - @Override - public ClassLoader getDeserializationClassLoader() { - return joinedClassLoader; - } - - @Override - public Serializable getId() { - return getClass().getName(); - } - - @Override - public Iterable> getOperationExecutionListeners() { - return operationExecutionListeners.keySet(); - } - - @Override - public void addOperationExecutionListener(OperationExecutionListener listener) { - operationExecutionListeners.put(listener, listener); - } - - @Override - public void removeOperationExecutionListener(OperationExecutionListener listener) { - operationExecutionListeners.remove(listener); - } - - @Override - public void serializeForInitialReplicationInternal(ObjectOutputStream oos) throws IOException { - StringBuffer logoutput = new StringBuffer(); - - logger.info("Serializing regattas..."); - oos.writeObject(regattasByName); - logoutput.append("Serialized " + regattasByName.size() + " regattas\n"); - for (Regatta regatta : regattasByName.values()) { - logoutput.append(String.format("%3s\n", regatta.toString())); - } - - logger.info("Serializing events..."); - oos.writeObject(eventsById); - logoutput.append("\nSerialized " + eventsById.size() + " events\n"); - for (Event event : eventsById.values()) { - logoutput.append(String.format("%3s\n", event.toString())); - } - - logger.info("Serializing regattas observed..."); - oos.writeObject(regattasObservedForDefaultLeaderboard); - logger.info("Serializing regatta tracking cache..."); - oos.writeObject(regattaTrackingCache); - logger.info("Serializing leaderboard groups..."); - oos.writeObject(leaderboardGroupsByName); - logoutput.append("Serialized " + leaderboardGroupsByName.size() + " leaderboard groups\n"); - for (LeaderboardGroup lg : leaderboardGroupsByName.values()) { - logoutput.append(String.format("%3s\n", lg.toString())); - } - logger.info("Serializing leaderboards..."); - oos.writeObject(leaderboardsByName); - logoutput.append("Serialized " + leaderboardsByName.size() + " leaderboards\n"); - for (Leaderboard lg : leaderboardsByName.values()) { - logoutput.append(String.format("%3s\n", lg.toString())); - } - logger.info("Serializing media library..."); - mediaLibrary.serialize(oos); - logoutput.append("Serialized " + mediaLibrary.allTracks().size() + " media tracks\n"); - for (MediaTrack lg : mediaLibrary.allTracks()) { - logoutput.append(String.format("%3s\n", lg.toString())); - } - logger.info("Serializing persisted competitors..."); - oos.writeObject(competitorStore); - logoutput.append("Serialized " + competitorStore.size() + " persisted competitors\n"); - - logger.info("Serializing configuration map..."); - oos.writeObject(configurationMap); - logoutput.append("Serialized " + configurationMap.size() + " configuration entries\n"); - for (DeviceConfigurationMatcher matcher : configurationMap.keySet()) { - logoutput.append(String.format("%3s\n", matcher.toString())); - } - - logger.info("Serializing remote sailing server references..."); - final ArrayList remoteServerReferences = new ArrayList<>(remoteSailingServerSet - .getCachedEventsForRemoteSailingServers().keySet()); - oos.writeObject(remoteServerReferences); - logoutput.append("Serialized " + remoteServerReferences.size() + " remote sailing server references\n"); - - logger.info(logoutput.toString()); - } - - @SuppressWarnings("unchecked") - // all the casts of ois.readObject()'s return value to Map<..., ...> - // the type-parameters in the casts of the de-serialized collection objects can't be checked - @Override - public void initiallyFillFromInternal(ObjectInputStream ois) throws IOException, ClassNotFoundException, - InterruptedException { - logger.info("Performing initial replication load on " + this); - // Use this object's class's class loader as the context class loader which will then be used for - // de-serialization; this will cause all classes to be visible that this bundle - // (com.sap.sailing.server) can see - StringBuffer logoutput = new StringBuffer(); - logger.info("Reading all regattas..."); - regattasByName.putAll((Map) ois.readObject()); - logoutput.append("Received " + regattasByName.size() + " NEW regattas\n"); - for (Regatta regatta : regattasByName.values()) { - logoutput.append(String.format("%3s\n", regatta.toString())); - } - - logger.info("Reading all events..."); - eventsById.putAll((Map) ois.readObject()); - logoutput.append("\nReceived " + eventsById.size() + " NEW events\n"); - for (Event event : eventsById.values()) { - logoutput.append(String.format("%3s\n", event.toString())); - } - - // it is important that the leaderboards and tracked regattas are cleared before auto-linking to - // old leaderboards takes place which then don't match the new ones - logger.info("Reading all dynamic tracked regattas..."); - for (DynamicTrackedRegatta trackedRegattaToObserve : (Set) ois.readObject()) { - ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(trackedRegattaToObserve); - } - - logger.info("Reading all of the regatta tracking cache..."); - regattaTrackingCache.putAll((Map) ois.readObject()); - logoutput.append("Received " + regattaTrackingCache.size() + " NEW regatta tracking cache entries\n"); - - logger.info("Reading leaderboard groups..."); - leaderboardGroupsByName.putAll((Map) ois.readObject()); - logoutput.append("Received " + leaderboardGroupsByName.size() + " NEW leaderboard groups\n"); - for (LeaderboardGroup lg : leaderboardGroupsByName.values()) { - leaderboardGroupsByID.put(lg.getId(), lg); - logoutput.append(String.format("%3s\n", lg.toString())); - } - - logger.info("Reading leaderboards by name..."); - leaderboardsByName.putAll((Map) ois.readObject()); - logoutput.append("Received " + leaderboardsByName.size() + " NEW leaderboards\n"); - for (Leaderboard leaderboard : leaderboardsByName.values()) { - logoutput.append(String.format("%3s\n", leaderboard.toString())); - } - - // now fix ScoreCorrectionListener setup for LeaderboardGroupMetaLeaderboard instances: - for (Leaderboard leaderboard : leaderboardsByName.values()) { - if (leaderboard instanceof LeaderboardGroupMetaLeaderboard) { - ((LeaderboardGroupMetaLeaderboard) leaderboard) - .registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards(); - } else if (leaderboard instanceof FlexibleLeaderboard) { - // and re-establish the RaceLogReplicator as listener on FlexibleLeaderboard objects - leaderboard.addRaceColumnListener(raceLogReplicator); - } - } - - logger.info("Reading media library..."); - mediaLibrary.deserialize(ois); - logoutput.append("Received " + mediaLibrary.allTracks().size() + " NEW media tracks\n"); - for (MediaTrack mediatrack : mediaLibrary.allTracks()) { - logoutput.append(String.format("%3s\n", mediatrack.toString())); - } - - // only copy the competitors from the deserialized competitor store; don't use it because it will have set - // a default Mongo object factory - logger.info("Reading competitors..."); - for (Competitor competitor : ((CompetitorStore) ois.readObject()).getCompetitors()) { - DynamicCompetitor dynamicCompetitor = (DynamicCompetitor) competitor; - // the following should actually be redundant because during de-serialization the Competitor objects, - // whose classes implement IsManagedByCache, should already have been got/created from/in the - // competitor store - competitorStore.getOrCreateCompetitor(dynamicCompetitor.getId(), dynamicCompetitor.getName(), - dynamicCompetitor.getColor(), dynamicCompetitor.getEmail(), dynamicCompetitor.getFlagImage(), - dynamicCompetitor.getTeam(), dynamicCompetitor.getBoat(), dynamicCompetitor.getTimeOnTimeFactor(), - dynamicCompetitor.getTimeOnDistanceAllowancePerNauticalMile()); - } - logoutput.append("Received " + competitorStore.size() + " NEW competitors\n"); - - logger.info("Reading device configurations..."); - configurationMap.putAll((DeviceConfigurationMapImpl) ois.readObject()); - logoutput.append("Received " + configurationMap.size() + " NEW configuration entries\n"); - for (DeviceConfigurationMatcher matcher : configurationMap.keySet()) { - logoutput.append(String.format("%3s\n", matcher.toString())); - } - - logger.info("Reading remote sailing server references..."); - for (RemoteSailingServerReference remoteSailingServerReference : (Iterable) ois - .readObject()) { - remoteSailingServerSet.add(remoteSailingServerReference); - logoutput.append("Received remote sailing server reference " + remoteSailingServerReference); - } - - // make sure to initialize listeners correctly - for (Regatta regatta : regattasByName.values()) { - RegattaImpl regattaImpl = (RegattaImpl) regatta; - regattaImpl.initializeSeriesAfterDeserialize(); - regattaImpl.addRaceColumnListener(raceLogReplicator); - } - // re-establish RaceLogResolver references to this RacingEventService in all TrackedRace instances - for (DynamicTrackedRegatta trackedRegatta : regattaTrackingCache.values()) { - trackedRegatta.lockTrackedRacesForRead(); - try { - for (TrackedRace trackedRace : trackedRegatta.getTrackedRaces()) { - ((TrackedRaceImpl) trackedRace).setRaceLogResolver(this); - } - } finally { - trackedRegatta.unlockTrackedRacesAfterRead(); - } - } - logger.info(logoutput.toString()); - } - - @Override - public void clearReplicaState() throws MalformedURLException, IOException, InterruptedException { - logger.info("Clearing all data structures..."); - LockUtil.lockForWrite(regattasByNameLock); - try { - regattasByName.clear(); - } finally { - LockUtil.unlockAfterWrite(regattasByNameLock); - } - regattasObservedForDefaultLeaderboard.clear(); - - if (raceTrackersByRegatta != null && !raceTrackersByRegatta.isEmpty()) { - for (DynamicTrackedRegatta regatta : regattaTrackingCache.values()) { - final Set trackers = raceTrackersByRegatta.get(regatta.getRegatta()); - if (trackers != null) { - for (RaceTracker tracker : trackers) { - tracker.stop(/* preemptive */true); - } - } - } - } - LockUtil.lockForWrite(regattaTrackingCacheLock); - try { - regattaTrackingCache.clear(); - } finally { - LockUtil.unlockAfterWrite(regattaTrackingCacheLock); - } - LockUtil.lockForWrite(raceTrackersByRegattaLock); - try { - raceTrackersByRegatta.clear(); - } finally { - LockUtil.unlockAfterWrite(raceTrackersByRegattaLock); - } - LockUtil.lockForWrite(leaderboardGroupsByNameLock); - try { - leaderboardGroupsByName.clear(); - leaderboardGroupsByID.clear(); - } finally { - LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock); - } - LockUtil.lockForWrite(leaderboardsByNameLock); - try { - leaderboardsByName.clear(); - } finally { - LockUtil.unlockAfterWrite(leaderboardsByNameLock); - } - eventsById.clear(); - mediaLibrary.clear(); - competitorStore.clear(); - remoteSailingServerSet.clear(); - } - - // Used for TESTING only - @Override - public Event addEvent(String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, - String venue, boolean isPublic, UUID id) { - Event result = createEventWithoutReplication(eventName, eventDescription, startDate, endDate, venue, isPublic, - id, /* officialWebsiteURL */null, /* sailorsInfoWebsiteURLAsString */null, - /* images */Collections. emptyList(), /* videos */Collections. emptyList()); - replicate(new CreateEvent(eventName, eventDescription, startDate, endDate, venue, isPublic, id, - /* officialWebsiteURLAsString */null, /* sailorsInfoWebsiteURLAsString */null, - /* images */Collections. emptyList(), /* videos */Collections. emptyList())); - return result; - } - - @Override - public void addEventWithoutReplication(Event event) { - addEvent(event); - } - - @Override - public Event createEventWithoutReplication(String eventName, String eventDescription, TimePoint startDate, - TimePoint endDate, String venue, boolean isPublic, UUID id, URL officialWebsiteURL, URL sailorsInfoWebsiteURL, - Iterable images, Iterable videos) { - Event result = new EventImpl(eventName, startDate, endDate, venue, isPublic, id); - addEvent(result); - result.setDescription(eventDescription); - result.setOfficialWebsiteURL(officialWebsiteURL); - result.setImages(images); - result.setVideos(videos); - return result; - } - - private void addEvent(Event result) { - if (eventsById.containsKey(result.getId())) { - throw new IllegalArgumentException("Event with ID " + result.getId() - + " already exists which is pretty surprising..."); - } - eventsById.put(result.getId(), result); - mongoObjectFactory.storeEvent(result); - } - - @Override - public void updateEvent(UUID id, String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, - String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, URL sailorsInfoWebsiteURL, - Iterable images, Iterable videos) { - final Event event = eventsById.get(id); - if (event == null) { - throw new IllegalArgumentException("Sailing event with ID " + id + " does not exist."); - } - event.setName(eventName); - event.setDescription(eventDescription); - event.setStartDate(startDate); - event.setEndDate(endDate); - event.setPublic(isPublic); - event.getVenue().setName(venueName); - List leaderboardGroups = new ArrayList<>(); - for (UUID lgid : leaderboardGroupIds) { - LeaderboardGroup lg = getLeaderboardGroupByID(lgid); - if (lg != null) { - leaderboardGroups.add(lg); - } else { - logger.info("Couldn't find leaderboard group with ID " + lgid + " while updating event " - + event.getName()); - } - } - event.setLeaderboardGroups(leaderboardGroups); - event.setOfficialWebsiteURL(officialWebsiteURL); - event.setSailorsInfoWebsiteURL(sailorsInfoWebsiteURL); - event.setImages(images); - event.setVideos(videos); - // TODO consider use diffutils to compute diff between old and new leaderboard groups list and apply the patch - // to keep changes minimial - mongoObjectFactory.storeEvent(event); - } - - @Override - public void renameEvent(UUID id, String newName) { - final Event toRename = eventsById.get(id); - if (toRename == null) { - throw new IllegalArgumentException("No sailing event with ID " + id + " found."); - } - toRename.setName(newName); - mongoObjectFactory.renameEvent(id, newName); - replicate(new RenameEvent(id, newName)); - } - - @Override - public void removeEvent(UUID id) { - removeEventFromEventsById(id); - mongoObjectFactory.removeEvent(id); - replicate(new RemoveEvent(id)); - } - - protected void removeEventFromEventsById(Serializable id) { - eventsById.remove(id); - } - - @Override - public Regatta getRememberedRegattaForRace(Serializable raceID) { - return persistentRegattasForRaceIDs.get(raceID.toString()); - } - - /** - * Persistently remembers the association of the race with its {@link RaceDefinition#getId()} to the - * regatta with its {@link Regatta#getRegattaIdentifier() identifier} so that the next time - * {@link #getRememberedRegattaForRace(RaceDefinition)} is called with race as argument, - * regatta will be returned. - */ - private void setRegattaForRace(Regatta regatta, RaceDefinition race) { - setRegattaForRace(regatta, race.getId().toString()); - } - - @Override - public void setRegattaForRace(Regatta regatta, String raceIdAsString) { - persistentRegattasForRaceIDs.put(raceIdAsString, regatta); - mongoObjectFactory.storeRegattaForRaceID(raceIdAsString, regatta); - } - - @Override - public CourseArea[] addCourseAreas(UUID eventId, String[] courseAreaNames, UUID[] courseAreaIds) { - final CourseArea[] courseAreas = addCourseAreasWithoutReplication(eventId, courseAreaIds, courseAreaNames); - replicate(new AddCourseAreas(eventId, courseAreaNames, courseAreaIds)); - return courseAreas; - } - - @Override - public CourseArea[] addCourseAreasWithoutReplication(UUID eventId, UUID[] courseAreaIds, String[] courseAreaNames) { - final CourseArea[] result = new CourseArea[courseAreaNames.length]; - for (int i=0; i mediaTracks) { - mediaLibrary.addMediaTracks(mediaTracks); - } - - @Override - public void mediaTrackTitleChanged(MediaTrack mediaTrack) { - mediaDB.updateTitle(mediaTrack.dbId, mediaTrack.title); - mediaLibrary.titleChanged(mediaTrack); - replicate(new UpdateMediaTrackTitleOperation(mediaTrack)); - } - - @Override - public void mediaTrackUrlChanged(MediaTrack mediaTrack) { - mediaDB.updateUrl(mediaTrack.dbId, mediaTrack.url); - mediaLibrary.urlChanged(mediaTrack); - replicate(new UpdateMediaTrackUrlOperation(mediaTrack)); - } - - @Override - public void mediaTrackStartTimeChanged(MediaTrack mediaTrack) { - mediaDB.updateStartTime(mediaTrack.dbId, mediaTrack.startTime); - mediaLibrary.startTimeChanged(mediaTrack); - replicate(new UpdateMediaTrackStartTimeOperation(mediaTrack)); - } - - @Override - public void mediaTrackDurationChanged(MediaTrack mediaTrack) { - mediaDB.updateDuration(mediaTrack.dbId, mediaTrack.duration); - mediaLibrary.durationChanged(mediaTrack); - replicate(new UpdateMediaTrackDurationOperation(mediaTrack)); - } - - @Override - public void mediaTrackAssignedRacesChanged(MediaTrack mediaTrack) { - mediaDB.updateRace(mediaTrack.dbId, mediaTrack.assignedRaces); - mediaLibrary.assignedRacesChanged(mediaTrack); - replicate(new UpdateMediaTrackRacesOperation(mediaTrack)); - - } - - @Override - public void mediaTrackDeleted(MediaTrack mediaTrack) { - mediaDB.deleteMediaTrack(mediaTrack.dbId); - mediaLibrary.deleteMediaTrack(mediaTrack); - replicate(new RemoveMediaTrackOperation(mediaTrack)); - } - - @Override - public void mediaTracksImported(Collection mediaTracksToImport, boolean override) { - for (MediaTrack trackToImport : mediaTracksToImport) { - MediaTrack existingTrack = mediaLibrary.lookupMediaTrack(trackToImport); - if (existingTrack == null) { - mediaDB.insertMediaTrackWithId(trackToImport.dbId, trackToImport.title, trackToImport.url, - trackToImport.startTime, trackToImport.duration, trackToImport.mimeType, - trackToImport.assignedRaces); - mediaTrackAdded(trackToImport); - } else if (override) { - - // Using fine-grained update methods. - // Rationale: Changes on more than one track property are rare - // and don't justify the introduction of a new set - // of methods (including replication). - if (!Util.equalsWithNull(existingTrack.title, trackToImport.title)) { - mediaTrackTitleChanged(trackToImport); - } - if (!Util.equalsWithNull(existingTrack.url, trackToImport.url)) { - mediaTrackUrlChanged(trackToImport); - } - if (!Util.equalsWithNull(existingTrack.startTime, trackToImport.startTime)) { - mediaTrackStartTimeChanged(trackToImport); - } - if (!Util.equalsWithNull(existingTrack.duration, trackToImport.duration)) { - mediaTrackDurationChanged(trackToImport); - } - if (!Util.equalsWithNull(existingTrack.assignedRaces, trackToImport.assignedRaces)) { - mediaTrackAssignedRacesChanged(trackToImport); - } - } - } - } - - @Override - public Collection getMediaTracksForRace(RegattaAndRaceIdentifier regattaAndRaceIdentifier) { - return mediaLibrary.findMediaTracksForRace(regattaAndRaceIdentifier); - } - - @Override - public Collection getMediaTracksInTimeRange(RegattaAndRaceIdentifier regattaAndRaceIdentifier) { - TrackedRace trackedRace = getExistingTrackedRace(regattaAndRaceIdentifier); - if (trackedRace != null) { - if (trackedRace.isLive(MillisecondsTimePoint.now())) { - return mediaLibrary.findLiveMediaTracks(); - } else { - TimePoint raceStart = trackedRace.getStartOfRace() == null ? trackedRace.getStartOfTracking() - : trackedRace.getStartOfRace(); - TimePoint raceEnd = trackedRace.getEndOfRace() == null ? trackedRace.getEndOfTracking() : trackedRace - .getEndOfRace(); - return mediaLibrary.findMediaTracksInTimeRange(raceStart, raceEnd); - } - } else { - return Collections.emptyList(); - } - } - - @Override - public Collection getAllMediaTracks() { - return mediaLibrary.allTracks(); - } - - public String toString() { - return "RacingEventService: " + this.hashCode() + " Build: " + ServerInfo.getBuildVersion(); - } - - @Override - public void reloadRaceLog(String leaderboardName, String raceColumnName, String fleetName) { - Leaderboard leaderboard = getLeaderboardByName(leaderboardName); - if (leaderboard != null) { - RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName); - if (raceColumn != null) { - Fleet fleetImpl = raceColumn.getFleetByName(fleetName); - RaceLog racelog = raceColumn.getRaceLog(fleetImpl); - if (racelog != null) { - raceColumn.reloadRaceLog(fleetImpl); - logger.info("Reloaded race log for fleet " + fleetImpl + " for race column " + raceColumn.getName() - + " for leaderboard " + leaderboard.getName()); - } - } - } - } - - @Override - public ConcurrentHashMap getPersistentRegattasForRaceIDs() { - return persistentRegattasForRaceIDs; - } - - @Override - public WindStore getWindStore() { - return windStore; - } - - @Override - public DeviceConfiguration getDeviceConfiguration(DeviceConfigurationIdentifier identifier) { - return configurationMap.getByMatch(identifier); - } - - @Override - public void createOrUpdateDeviceConfiguration(DeviceConfigurationMatcher matcher, DeviceConfiguration configuration) { - configurationMap.put(matcher, configuration); - mongoObjectFactory.storeDeviceConfiguration(matcher, configuration); - replicate(new CreateOrUpdateDeviceConfiguration(matcher, configuration)); - } - - @Override - public void removeDeviceConfiguration(DeviceConfigurationMatcher matcher) { - configurationMap.remove(matcher); - mongoObjectFactory.removeDeviceConfiguration(matcher); - replicate(new RemoveDeviceConfiguration(matcher)); - } - - @Override - public Map getAllDeviceConfigurations() { - return new HashMap(configurationMap); - } - - @Override - public TimePoint setStartTimeAndProcedure(String leaderboardName, String raceColumnName, String fleetName, - String authorName, int authorPriority, int passId, TimePoint logicalTimePoint, TimePoint startTime, - RacingProcedureType racingProcedure) { - RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); - Leaderboard leaderboard = getLeaderboardByName(leaderboardName); - final TimePoint result; - if (leaderboard instanceof HasRegattaLike && raceLog != null) { - RaceState state = RaceStateImpl.create(/* race log resolver */ this, raceLog, new LogEventAuthorImpl(authorName, authorPriority)); - if (passId > raceLog.getCurrentPassId()) { - state.setAdvancePass(logicalTimePoint); - } - state.setRacingProcedure(logicalTimePoint, racingProcedure); - state.forceNewStartTime(logicalTimePoint, startTime); - result = state.getStartTime(); - } else { - result = null; - } - return result; - } - - public RaceLog getRaceLog(String leaderboardName, String raceColumnName, String fleetName) { - Leaderboard leaderboard = getLeaderboardByName(leaderboardName); - if (leaderboard != null) { - RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName); - if (raceColumn != null) { - Fleet fleetImpl = raceColumn.getFleetByName(fleetName); - return raceColumn.getRaceLog(fleetImpl); - } - } - return null; - } - - @Override - public com.sap.sse.common.Util.Triple getStartTimeAndProcedure( - String leaderboardName, String raceColumnName, String fleetName) { - RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); - Leaderboard leaderboard = getLeaderboardByName(leaderboardName); - final Triple result; - if (leaderboard instanceof HasRegattaLike && raceLog != null) { - ReadonlyRaceState state = ReadonlyRaceStateImpl.create(/* race log resolver */ this, raceLog); - result = new com.sap.sse.common.Util.Triple(state.getStartTime(), - raceLog.getCurrentPassId(), state.getRacingProcedure().getType()); - } else { - result = null; - } - return result; - } - - private Iterable getWindTrackerFactories() { - final Set result; - if (bundleContext == null) { - result = Collections.singleton((WindTrackerFactory) ExpeditionWindTrackerFactory.getInstance()); - } else { - ServiceTracker tracker = new ServiceTracker( - bundleContext, WindTrackerFactory.class.getName(), null); - tracker.open(); - result = new HashSet<>(); - for (WindTrackerFactory factory : tracker.getServices(new WindTrackerFactory[0])) { - result.add(factory); - } - } - return result; - } - - @Override - public GPSFixStore getGPSFixStore() { - return gpsFixStore; - } - - @Override - public RaceTracker getRaceTrackerById(Object id) { - return raceTrackersByID.get(id); - } - - @Override - public AbstractLogEventAuthor getServerAuthor() { - return raceLogEventAuthorForServer; - } - - @Override - public CompetitorStore getCompetitorStore() { - return competitorStore; - } - - @Override - public TypeBasedServiceFinderFactory getTypeBasedServiceFinderFactory() { - return serviceFinderFactory; - } - - @Override - public DataImportLockWithProgress getDataImportLock() { - return dataImportLock; - } - - @Override - public DataImportProgress createOrUpdateDataImportProgressWithReplication(UUID importOperationId, - double overallProgressPct, String subProgressName, double subProgressPct) { - // Create/Update locally - DataImportProgress progress = createOrUpdateDataImportProgressWithoutReplication(importOperationId, - overallProgressPct, subProgressName, subProgressPct); - // Create/Update on replicas - replicate(new CreateOrUpdateDataImportProgress(importOperationId, overallProgressPct, subProgressName, - subProgressPct)); - return progress; - } - - @Override - public DataImportProgress createOrUpdateDataImportProgressWithoutReplication(UUID importOperationId, - double overallProgressPct, String subProgressName, double subProgressPct) { - DataImportProgress progress = dataImportLock.getProgress(importOperationId); - boolean newObject = false; - if (progress == null) { - progress = new DataImportProgressImpl(importOperationId); - newObject = true; - } - progress.setOverAllProgressPct(overallProgressPct); - progress.setNameOfCurrentSubProgress(subProgressName); - progress.setCurrentSubProgressPct(subProgressPct); - if (newObject) { - dataImportLock.addProgress(importOperationId, progress); - } - return progress; - } - - @Override - public void setDataImportFailedWithoutReplication(UUID importOperationId, String errorMessage) { - DataImportProgress progress = dataImportLock.getProgress(importOperationId); - if (progress != null) { - progress.setFailed(); - progress.setErrorMessage(errorMessage); - } - } - - @Override - public void setDataImportFailedWithReplication(UUID importOperationId, String errorMessage) { - setDataImportFailedWithoutReplication(importOperationId, errorMessage); - replicate(new DataImportFailed(importOperationId, errorMessage)); - } - - @Override - public void setDataImportDeleteProgressFromMapTimerWithReplication(UUID importOperationId) { - setDataImportDeleteProgressFromMapTimerWithoutReplication(importOperationId); - replicate(new SetDataImportDeleteProgressFromMapTimer(importOperationId)); - } - - @Override - public void setDataImportDeleteProgressFromMapTimerWithoutReplication(UUID importOperationId) { - dataImportLock.setDeleteFromMapTimer(importOperationId); - } - - @Override - public Result search(KeywordQuery query) { - long start = System.currentTimeMillis(); - logger.info("Searching local server for " + query); - Result result = new RegattaByKeywordSearchService().search(this, query); - logger.fine("Search for " + query + " took " + (System.currentTimeMillis() - start) + "ms"); - return result; - } - - @Override - public Result searchRemotely(String remoteServerReferenceName, KeywordQuery query) { - long start = System.currentTimeMillis(); - ResultImpl result = null; - RemoteSailingServerReference remoteRef = remoteSailingServerSet - .getServerReferenceByName(remoteServerReferenceName); - if (remoteRef == null) { - result = null; - } else { - BufferedReader bufferedReader = null; - try { - try { - final URL eventsURL = new URL(remoteRef.getURL(), "sailingserver/api/v1/search?q=" - + URLEncoder.encode(query.toString(), "UTF-8")); - logger.info("Searching remote server " + remoteRef + " for " + query); - URLConnection urlConnection = eventsURL.openConnection(); - urlConnection.connect(); - bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), "UTF-8")); - JSONParser parser = new JSONParser(); - Object eventsAsObject = parser.parse(bufferedReader); - final LeaderboardGroupBaseJsonDeserializer leaderboardGroupBaseJsonDeserializer = new LeaderboardGroupBaseJsonDeserializer(); - LeaderboardSearchResultBaseJsonDeserializer deserializer = new LeaderboardSearchResultBaseJsonDeserializer( - new EventBaseJsonDeserializer(new VenueJsonDeserializer(new CourseAreaJsonDeserializer( - DomainFactory.INSTANCE)), leaderboardGroupBaseJsonDeserializer), - leaderboardGroupBaseJsonDeserializer); - result = new ResultImpl(query, - new LeaderboardSearchResultBaseRanker()); - JSONArray hitsAsJsonArray = (JSONArray) eventsAsObject; - for (Object hitAsObject : hitsAsJsonArray) { - JSONObject hitAsJson = (JSONObject) hitAsObject; - LeaderboardSearchResultBase hit = deserializer.deserialize(hitAsJson); - result.addHit(hit); - } - } finally { - if (bufferedReader != null) { - bufferedReader.close(); - } - } - } catch (IOException | ParseException e) { - logger.log(Level.INFO, - "Exception trying to fetch events from remote server " + remoteRef + ": " + e.getMessage(), e); - } - } - logger.fine("Remote search on " + remoteRef + " for " + query + " took " + (System.currentTimeMillis() - start) - + "ms"); - return result; - } - - @Override - public ReplicationMasterDescriptor getMasterDescriptor() { - return replicatingFromMaster; - } - - @Override - public void startedReplicatingFrom(ReplicationMasterDescriptor master) { - this.replicatingFromMaster = master; - } - - @Override - public void stoppedReplicatingFrom(ReplicationMasterDescriptor master) { - this.replicatingFromMaster = null; - } - - @Override - public void addOperationSentToMasterForReplication( - OperationWithResultWithIdWrapper operationWithResultWithIdWrapper) { - this.operationsSentToMasterForReplication.add(operationWithResultWithIdWrapper); - } - - @Override - public boolean hasSentOperationToMaster(OperationWithResult operation) { - return this.operationsSentToMasterForReplication.remove(operation); - } - - @Override - public FileStorageManagementService getFileStorageManagementService() { - ServiceReference ref = bundleContext - .getServiceReference(FileStorageManagementService.class); - if (ref == null) { - logger.warning("No file storage management service registered"); - return null; - } - return bundleContext.getService(ref); - } - - public void addMasterDataClassLoader(ClassLoader classLoader) { - masterDataClassLoaders.add(classLoader); - } - - public void removeMasterDataClassLoader(ClassLoader classLoader) { - masterDataClassLoaders.remove(classLoader); - } - - @Override - public ClassLoader getCombinedMasterDataClassLoader() { - JoinedClassLoader joinedClassLoader = new JoinedClassLoader(masterDataClassLoaders); - return joinedClassLoader; - } - - public void setPolarDataService(PolarDataService service) { - if (this.polarDataService == null && service != null) { - polarDataService = service; - polarDataService.registerDomainFactory(baseDomainFactory); - setPolarDataServiceOnAllTrackedRaces(service); - } - } - - private void setPolarDataServiceOnAllTrackedRaces(PolarDataService service) { - Iterable allRegattas = getAllRegattas(); - for (Regatta regatta : allRegattas) { - DynamicTrackedRegatta trackedRegatta = getTrackedRegatta(regatta); - if (trackedRegatta != null) { - trackedRegatta.lockTrackedRacesForRead(); - try { - Iterable trackedRaces = trackedRegatta.getTrackedRaces(); - for (TrackedRace trackedRace : trackedRaces) { - trackedRace.setPolarDataService(service); - service.insertExistingFixes(trackedRace); - } - } finally { - trackedRegatta.unlockTrackedRacesAfterRead(); - } - } - } - } - - public void unsetPolarDataService(PolarDataService service) { - if (polarDataService == service) { - polarDataService.unregisterDomainFactory(baseDomainFactory); - polarDataService = null; - setPolarDataService(null); - } - } - - @Override - public Iterable getCompetitorInOrderOfWindwardDistanceTraveledFarthestFirst(TrackedRace trackedRace, TimePoint timePoint) { - final RankingInfo rankingInfo = trackedRace.getRankingMetric().getRankingInfo(timePoint); - final List result = new ArrayList<>(); - final Map windwardDistanceSailedPerCompetitor = new HashMap<>(); - for (final Competitor competitor : trackedRace.getRace().getCompetitors()) { - result.add(competitor); - final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); - windwardDistanceSailedPerCompetitor.put(competitor, competitorRankingInfo == null ? null : competitorRankingInfo.getWindwardDistanceSailed()); - } - final Comparator durationComparatorNullsLast = Comparator.nullsLast(Comparator.naturalOrder()); - result.sort((c1, c2) -> durationComparatorNullsLast.compare(windwardDistanceSailedPerCompetitor.get(c2), - windwardDistanceSailedPerCompetitor.get(c1))); - return result; - } - - /** - * A {@link SimpleRaceLogIdentifier} in particular has a {@link SimpleRaceLogIdentifier#getRegattaLikeParentName()} - * which identifies either a regatta by name or a flexible leaderboard by name. Here is why this can luckily be - * resolved unanimously: A regatta leaderboard always uses as its name the regatta name (see - * {@link RegattaImpl#getName()}). Trying to {@link RegattaLeaderboardImpl#setName(String) set} the regatta leaderboard's - * name can only update its {@link Leaderboard#getDisplayName() display name}. Therefore, regatta leaderboards are always - * keyed in {@link #leaderboardsByName} by their regatta's name. Thus, no flexible leaderboard can have a regatta's name - * as its name, and therefore leaderboard names and regatta names are unitedly unique. - */ - @Override - public RaceLog resolve(SimpleRaceLogIdentifier identifier) { - final RaceLog result; - final IsRegattaLike regattaLike; - final Regatta regatta = regattasByName.get(identifier.getRegattaLikeParentName()); - if (regatta != null) { - regattaLike = regatta; - } else { - final Leaderboard leaderboard = leaderboardsByName.get(identifier.getRegattaLikeParentName()); - if (leaderboard != null && leaderboard instanceof FlexibleLeaderboard) { - regattaLike = (FlexibleLeaderboard) leaderboard; - } else { - regattaLike = null; - } - } - if (regattaLike != null) { - final RaceColumn raceColumn = regattaLike.getRaceColumnByName(identifier.getRaceColumnName()); - if (raceColumn != null) { - final Fleet fleet = raceColumn.getFleetByName(identifier.getFleetName()); - if (fleet != null) { - result = raceColumn.getRaceLog(fleet); - } else { - result = null; - } - } else { - result = null; - } - } else { - result = null; - } - return result; - } -} +package com.sap.sailing.server.impl; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.net.MalformedURLException; +import java.net.SocketException; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +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 java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +import com.sap.sailing.domain.abstractlog.AbstractLog; +import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; +import com.sap.sailing.domain.abstractlog.impl.LogEventAuthorImpl; +import com.sap.sailing.domain.abstractlog.race.RaceLog; +import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogEventVisitor; +import com.sap.sailing.domain.abstractlog.race.SimpleRaceLogIdentifier; +import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; +import com.sap.sailing.domain.abstractlog.race.state.RaceState; +import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; +import com.sap.sailing.domain.abstractlog.race.state.impl.RaceStateImpl; +import com.sap.sailing.domain.abstractlog.race.state.impl.ReadonlyRaceStateImpl; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorStore; +import com.sap.sailing.domain.base.CompetitorStore.CompetitorUpdateListener; +import com.sap.sailing.domain.base.ControlPoint; +import com.sap.sailing.domain.base.CourseArea; +import com.sap.sailing.domain.base.DomainFactory; +import com.sap.sailing.domain.base.Event; +import com.sap.sailing.domain.base.EventBase; +import com.sap.sailing.domain.base.EventFetcher; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.LeaderboardSearchResult; +import com.sap.sailing.domain.base.LeaderboardSearchResultBase; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnInSeries; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.RegattaListener; +import com.sap.sailing.domain.base.RemoteSailingServerReference; +import com.sap.sailing.domain.base.SailingServerConfiguration; +import com.sap.sailing.domain.base.Series; +import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.base.configuration.DeviceConfiguration; +import com.sap.sailing.domain.base.configuration.DeviceConfigurationIdentifier; +import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; +import com.sap.sailing.domain.base.configuration.RegattaConfiguration; +import com.sap.sailing.domain.base.configuration.impl.DeviceConfigurationMapImpl; +import com.sap.sailing.domain.base.impl.DynamicCompetitor; +import com.sap.sailing.domain.base.impl.EventImpl; +import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.base.impl.RemoteSailingServerReferenceImpl; +import com.sap.sailing.domain.common.DataImportProgress; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.RegattaIdentifier; +import com.sap.sailing.domain.common.RegattaName; +import com.sap.sailing.domain.common.Renamable; +import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.common.TrackedRaceStatusEnum; +import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.dto.FleetDTO; +import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; +import com.sap.sailing.domain.common.dto.SeriesCreationParametersDTO; +import com.sap.sailing.domain.common.impl.DataImportProgressImpl; +import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.racelog.RacingProcedureType; +import com.sap.sailing.domain.common.tracking.GPSFix; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.leaderboard.FlexibleLeaderboard; +import com.sap.sailing.domain.leaderboard.FlexibleRaceColumn; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.leaderboard.LeaderboardRegistry; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.impl.FlexibleLeaderboardImpl; +import com.sap.sailing.domain.leaderboard.impl.LeaderboardGroupImpl; +import com.sap.sailing.domain.leaderboard.impl.RegattaLeaderboardImpl; +import com.sap.sailing.domain.leaderboard.impl.ThresholdBasedResultDiscardingRuleImpl; +import com.sap.sailing.domain.leaderboard.meta.LeaderboardGroupMetaLeaderboard; +import com.sap.sailing.domain.persistence.DomainObjectFactory; +import com.sap.sailing.domain.persistence.MongoObjectFactory; +import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; +import com.sap.sailing.domain.persistence.MongoRegattaLogStoreFactory; +import com.sap.sailing.domain.persistence.MongoWindStore; +import com.sap.sailing.domain.persistence.MongoWindStoreFactory; +import com.sap.sailing.domain.persistence.PersistenceFactory; +import com.sap.sailing.domain.persistence.media.MediaDB; +import com.sap.sailing.domain.persistence.media.MediaDBFactory; +import com.sap.sailing.domain.persistence.racelog.tracking.MongoGPSFixStoreFactory; +import com.sap.sailing.domain.polars.PolarDataService; +import com.sap.sailing.domain.racelog.RaceLogIdentifier; +import com.sap.sailing.domain.racelog.RaceLogStore; +import com.sap.sailing.domain.racelog.tracking.GPSFixStore; +import com.sap.sailing.domain.racelogtracking.DeviceIdentifier; +import com.sap.sailing.domain.ranking.RankingMetric.CompetitorRankingInfo; +import com.sap.sailing.domain.ranking.RankingMetric.RankingInfo; +import com.sap.sailing.domain.ranking.RankingMetricConstructor; +import com.sap.sailing.domain.regattalike.HasRegattaLike; +import com.sap.sailing.domain.regattalike.IsRegattaLike; +import com.sap.sailing.domain.regattalike.LeaderboardThatHasRegattaLike; +import com.sap.sailing.domain.regattalog.RegattaLogStore; +import com.sap.sailing.domain.tracking.DynamicTrackedRace; +import com.sap.sailing.domain.tracking.DynamicTrackedRegatta; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.RaceChangeListener; +import com.sap.sailing.domain.tracking.RaceHandle; +import com.sap.sailing.domain.tracking.RaceListener; +import com.sap.sailing.domain.tracking.RaceTracker; +import com.sap.sailing.domain.tracking.RaceTrackingConnectivityParameters; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.TrackedRaceStatus; +import com.sap.sailing.domain.tracking.TrackedRegatta; +import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.domain.tracking.WindTracker; +import com.sap.sailing.domain.tracking.WindTrackerFactory; +import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; +import com.sap.sailing.domain.tracking.impl.DynamicGPSFixTrackImpl; +import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl; +import com.sap.sailing.domain.tracking.impl.TrackedRaceImpl; +import com.sap.sailing.expeditionconnector.ExpeditionWindTrackerFactory; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.Replicator; +import com.sap.sailing.server.gateway.deserialization.impl.CourseAreaJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.EventBaseJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardGroupBaseJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.LeaderboardSearchResultBaseJsonDeserializer; +import com.sap.sailing.server.gateway.deserialization.impl.VenueJsonDeserializer; +import com.sap.sailing.server.masterdata.DataImportLockWithProgress; +import com.sap.sailing.server.operationaltransformation.AddCourseAreas; +import com.sap.sailing.server.operationaltransformation.AddDefaultRegatta; +import com.sap.sailing.server.operationaltransformation.AddMediaTrackOperation; +import com.sap.sailing.server.operationaltransformation.AddRaceDefinition; +import com.sap.sailing.server.operationaltransformation.AddSpecificRegatta; +import com.sap.sailing.server.operationaltransformation.ConnectTrackedRaceToLeaderboardColumn; +import com.sap.sailing.server.operationaltransformation.CreateEvent; +import com.sap.sailing.server.operationaltransformation.CreateOrUpdateDataImportProgress; +import com.sap.sailing.server.operationaltransformation.CreateOrUpdateDeviceConfiguration; +import com.sap.sailing.server.operationaltransformation.CreateTrackedRace; +import com.sap.sailing.server.operationaltransformation.DataImportFailed; +import com.sap.sailing.server.operationaltransformation.RecordCompetitorGPSFix; +import com.sap.sailing.server.operationaltransformation.RecordMarkGPSFix; +import com.sap.sailing.server.operationaltransformation.RecordMarkGPSFixForExistingTrack; +import com.sap.sailing.server.operationaltransformation.RecordMarkGPSFixForNewMarkTrack; +import com.sap.sailing.server.operationaltransformation.RecordWindFix; +import com.sap.sailing.server.operationaltransformation.RemoveDeviceConfiguration; +import com.sap.sailing.server.operationaltransformation.RemoveEvent; +import com.sap.sailing.server.operationaltransformation.RemoveMediaTrackOperation; +import com.sap.sailing.server.operationaltransformation.RemoveWindFix; +import com.sap.sailing.server.operationaltransformation.RenameEvent; +import com.sap.sailing.server.operationaltransformation.SetDataImportDeleteProgressFromMapTimer; +import com.sap.sailing.server.operationaltransformation.TrackRegatta; +import com.sap.sailing.server.operationaltransformation.UpdateCompetitor; +import com.sap.sailing.server.operationaltransformation.UpdateEndOfTracking; +import com.sap.sailing.server.operationaltransformation.UpdateMarkPassings; +import com.sap.sailing.server.operationaltransformation.UpdateMediaTrackDurationOperation; +import com.sap.sailing.server.operationaltransformation.UpdateMediaTrackRacesOperation; +import com.sap.sailing.server.operationaltransformation.UpdateMediaTrackStartTimeOperation; +import com.sap.sailing.server.operationaltransformation.UpdateMediaTrackTitleOperation; +import com.sap.sailing.server.operationaltransformation.UpdateMediaTrackUrlOperation; +import com.sap.sailing.server.operationaltransformation.UpdateRaceDelayToLive; +import com.sap.sailing.server.operationaltransformation.UpdateStartOfTracking; +import com.sap.sailing.server.operationaltransformation.UpdateStartTimeReceived; +import com.sap.sailing.server.operationaltransformation.UpdateTrackedRaceStatus; +import com.sap.sailing.server.operationaltransformation.UpdateWindAveragingTime; +import com.sap.sailing.server.operationaltransformation.UpdateWindSourcesToExclude; +import com.sap.sailing.server.simulation.SimulationService; +import com.sap.sailing.server.simulation.SimulationServiceFactory; +import com.sap.sse.ServerInfo; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.TypeBasedServiceFinderFactory; +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.impl.MillisecondsTimePoint; +import com.sap.sse.common.search.KeywordQuery; +import com.sap.sse.common.search.Result; +import com.sap.sse.common.search.ResultImpl; +import com.sap.sse.concurrent.LockUtil; +import com.sap.sse.concurrent.NamedReentrantReadWriteLock; +import com.sap.sse.filestorage.FileStorageManagementService; +import com.sap.sse.replication.OperationExecutionListener; +import com.sap.sse.replication.OperationWithResult; +import com.sap.sse.replication.ReplicationMasterDescriptor; +import com.sap.sse.replication.impl.OperationWithResultWithIdWrapper; +import com.sap.sse.shared.media.ImageDescriptor; +import com.sap.sse.shared.media.VideoDescriptor; +import com.sap.sse.util.ClearStateTestSupport; +import com.sap.sse.util.JoinedClassLoader; +import com.sap.sse.util.impl.ThreadFactoryWithPriority; + +public class RacingEventServiceImpl implements RacingEventService, ClearStateTestSupport, RegattaListener, + LeaderboardRegistry, Replicator, EventFetcher { + private static final Logger logger = Logger.getLogger(RacingEventServiceImpl.class.getName()); + + /** + * A scheduler for the periodic checks of the paramURL documents for the advent of {@link ControlPoint}s with static + * position information otherwise not available through MarkPassingReceiver's events. + */ + private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, new ThreadFactoryWithPriority(Thread.NORM_PRIORITY, /* daemon */ true)); + + private final com.sap.sailing.domain.base.DomainFactory baseDomainFactory; + + /** + * Holds the {@link Event} objects for those event registered with this service. Note that there may be + * {@link Event} objects that exist outside this service for events not (yet) registered here. + */ + private final ConcurrentHashMap eventsById; + + private final RemoteSailingServerSet remoteSailingServerSet; + + /** + * Holds the {@link Regatta} objects for those races registered with this service. Note that there may be + * {@link Regatta} objects that exist outside this service for regattas not (yet) registered here. + */ + protected final ConcurrentHashMap regattasByName; + + private final NamedReentrantReadWriteLock regattasByNameLock; + + private final ConcurrentHashMap courseListeners; + + protected final ConcurrentHashMap> raceTrackersByRegatta; + + /** + * Although {@link #raceTrackersByRegatta} is a concurrent hash map, entering sets as values needs to be + * synchronized using this lock's write lock to avoid two value sets overwriting each other. + */ + private final NamedReentrantReadWriteLock raceTrackersByRegattaLock; + + /** + * Remembers the trackers by paramURL/liveURI/storedURI to avoid duplication + */ + protected final ConcurrentHashMap raceTrackersByID; + + /** + * {@link #addRace(RegattaIdentifier, RaceTrackingConnectivityParameters, long)} will check + * {@link #raceTrackersByID} for the presence of a tracker and won't create a new tracker if one for the + * connectivity parameters' ID already exists. This check and creation and addition to {@link #raceTrackersByID} + * requires locking in the face of concurrent calls to + * {@link #addRace(RegattaIdentifier, RaceTrackingConnectivityParameters, long)}. Using synchronized is + * not ideal due to its coarse-grained locking style which allows for little concurrency. Instead, this map is used + * to keep locks for any ID that any invocation of the + * {@link #addRace(RegattaIdentifier, RaceTrackingConnectivityParameters, long)} method is currently working on. + * Fetching or creating and putting a lock to this map happens in ({@link #getOrCreateRaceTrackersByIdLock}) which + * takes care of managing concurrent access to this concurrent map. When done, the + * {@link #addRace(RegattaIdentifier, RaceTrackingConnectivityParameters, long)} method cleans up by removing the + * lock again from this map, again using a synchronized method ( + * {@link #unlockRaceTrackersById(Object, NamedReentrantReadWriteLock)}). + */ + private final ConcurrentHashMap raceTrackersByIDLocks; + + /** + * Leaderboards managed by this racing event service + */ + private final ConcurrentHashMap leaderboardsByName; + + /** + * {@link #leaderboardsByName} is already a concurrent hash map; however, when renaming a leaderboard, this shall + * happen as an atomic transaction, not interruptible by other write accesses on the same map because otherwise + * assumptions made during the rename process wouldn't hold. See, in particular, + * {@link #renameLeaderboard(String, String)}. + */ + private final NamedReentrantReadWriteLock leaderboardsByNameLock; + + private final ConcurrentHashMap leaderboardGroupsByName; + + private final ConcurrentHashMap leaderboardGroupsByID; + + /** + * See {@link #leaderboardsByNameLock} + */ + private final NamedReentrantReadWriteLock leaderboardGroupsByNameLock; + + private final CompetitorStore competitorStore; + + /** + * A set based on a concurrent hash map, therefore being thread safe + */ + private Set regattasObservedForDefaultLeaderboard = Collections + .newSetFromMap(new ConcurrentHashMap()); + + private final MongoObjectFactory mongoObjectFactory; + + private final DomainObjectFactory domainObjectFactory; + + private final ConcurrentHashMap regattaTrackingCache; + + /** + * Protects write access transactions that do a previous read to {@link #regattaTrackingCache}; read-only access is + * already synchronized by using a concurrent hash map for {@link #regattaTrackingCache}. + */ + private final NamedReentrantReadWriteLock regattaTrackingCacheLock; + + private final ConcurrentHashMap, OperationExecutionListener> operationExecutionListeners; + + /** + * Keys are the toString() representation of the {@link RaceDefinition#getId() IDs} of races passed to + * {@link #setRegattaForRace(Regatta, RaceDefinition)}. + */ + private final ConcurrentHashMap persistentRegattasForRaceIDs; + + private final RaceLogReplicator raceLogReplicator; + private final RegattaLogReplicator regattaLogReplicator; + + private final RaceLogScoringReplicator raceLogScoringReplicator; + + private final MediaDB mediaDB; + + private final MediaLibrary mediaLibrary; + + /** + * Currently valid pairs of {@link DeviceConfigurationMatcher}s and {@link DeviceConfiguration}s. The contents of + * this map is persisted and replicated. See {@link DeviceConfigurationMapImpl}. + */ + protected final DeviceConfigurationMapImpl configurationMap; + + private final WindStore windStore; + private final GPSFixStore gpsFixStore; + + /** + * This author should be used for server generated race log events + */ + private final AbstractLogEventAuthor raceLogEventAuthorForServer = new LogEventAuthorImpl( + RacingEventService.class.getName(), 0); + + private PolarDataService polarDataService; + + private final SimulationService simulationService; + + /** + * Allow only one master data import at a time to avoid situation where multiple Imports override each other in + * unpredictable fashion + */ + private final DataImportLockWithProgress dataImportLock; + + /** + * If this service runs in the context of an OSGi environment, the activator should {@link #setBundleContext set the + * bundle context} on this object so that service lookups become possible. + */ + private BundleContext bundleContext; + + private TypeBasedServiceFinderFactory serviceFinderFactory; + + /** + * The master from which this replicable is currently replicating, or null if this replicable is not + * currently replicated from any master. + */ + private ReplicationMasterDescriptor replicatingFromMaster; + + private Set> operationsSentToMasterForReplication; + + private ThreadLocal currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster = ThreadLocal + .withInitial(() -> false); + + private final Set masterDataClassLoaders = new HashSet(); + + private final JoinedClassLoader joinedClassLoader; + + private SailingServerConfiguration sailingServerConfiguration; + + /** + * Providing the constructor parameters for a new {@link RacingEventServiceImpl} instance is a bit tricky + * in some cases because containment and initialization order of some types is fairly tightly coupled. + * There is a dependency of many such objects on an instance of {@link RaceLogResolver} which is implemented + * by {@link RacingEventServiceImpl}. However, therefore, this instance only becomes available in the + * innermost constructor. + * + * @author Axel Uhl (d043530) + * + */ + public static interface ConstructorParameters { + DomainObjectFactory getDomainObjectFactory(); + MongoObjectFactory getMongoObjectFactory(); + com.sap.sailing.domain.base.DomainFactory getBaseDomainFactory(); + CompetitorStore getCompetitorStore(); + } + + /** + * Constructs a {@link DomainFactory base domain factory} that uses this object's {@link #competitorStore competitor + * store} for competitor management. This base domain factory is then also used for the construction of the + * {@link DomainObjectFactory}. This constructor variant initially clears the persistent competitor collection, + * hence removes all previously persistent competitors. This is the default for testing and for backward + * compatibility with prior releases that did not support a persistent competitor collection. + */ + public RacingEventServiceImpl() { + this(true, null); + } + + public RacingEventServiceImpl(WindStore windStore, GPSFixStore gpsFixStore, + TypeBasedServiceFinderFactory serviceFinderFactory) { + this(true, windStore, gpsFixStore, serviceFinderFactory); + } + + void setBundleContext(BundleContext bundleContext) { + this.bundleContext = bundleContext; + } + + /** + * Like {@link #RacingEventServiceImpl()}, but allows callers to specify that the persistent competitor collection + * be cleared before the service starts. + * + * @param clearPersistentCompetitorStore + * if true, the {@link PersistentCompetitorStore} is created empty, with the corresponding + * database collection cleared as well. Use with caution! When used with false, competitors + * created and stored during previous service executions will initially be loaded. + */ + public RacingEventServiceImpl(boolean clearPersistentCompetitorStore, final TypeBasedServiceFinderFactory serviceFinderFactory) { + this((final RaceLogResolver raceLogResolver)-> { + return new ConstructorParameters() { + private final MongoObjectFactory mongoObjectFactory = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(serviceFinderFactory); + private final PersistentCompetitorStore competitorStore = new PersistentCompetitorStore( + PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(serviceFinderFactory), + clearPersistentCompetitorStore, serviceFinderFactory, raceLogResolver); + + @Override public DomainObjectFactory getDomainObjectFactory() { return competitorStore.getDomainObjectFactory(); } + @Override public MongoObjectFactory getMongoObjectFactory() { return mongoObjectFactory; } + @Override public DomainFactory getBaseDomainFactory() { return competitorStore.getBaseDomainFactory(); } + @Override public CompetitorStore getCompetitorStore() { return competitorStore; } + }; + }, MediaDBFactory.INSTANCE.getDefaultMediaDB(), null, null, serviceFinderFactory); + } + + private RacingEventServiceImpl(final boolean clearPersistentCompetitorStore, WindStore windStore, + GPSFixStore gpsFixStore, final TypeBasedServiceFinderFactory serviceFinderFactory) { + this((final RaceLogResolver raceLogResolver)-> { + return new ConstructorParameters() { + private final MongoObjectFactory mongoObjectFactory = PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory(serviceFinderFactory); + private final PersistentCompetitorStore competitorStore = new PersistentCompetitorStore( + mongoObjectFactory, + clearPersistentCompetitorStore, serviceFinderFactory, raceLogResolver); + + @Override public DomainObjectFactory getDomainObjectFactory() { return competitorStore.getDomainObjectFactory(); } + @Override public MongoObjectFactory getMongoObjectFactory() { return mongoObjectFactory; } + @Override public DomainFactory getBaseDomainFactory() { return competitorStore.getBaseDomainFactory(); } + @Override public CompetitorStore getCompetitorStore() { return competitorStore; } + }; + }, MediaDBFactory.INSTANCE.getDefaultMediaDB(), windStore, gpsFixStore, serviceFinderFactory); + } + + /** + * Uses the default factories for the tracking adapters and the {@link DomainFactory base domain factory} of the + * {@link PersistenceFactory#getDefaultDomainObjectFactory() default domain object factory}. This constructor should + * be used for testing because it provides a transient {@link CompetitorStore} as required for competitor + * persistence. + */ + public RacingEventServiceImpl(Function domainObjectFactoryProvider, + final MongoObjectFactory mongoObjectFactory, MediaDB mediaDB, WindStore windStore, GPSFixStore gpsFixStore) { + this((final RaceLogResolver raceLogResolver)-> { + return new ConstructorParameters() { + private final DomainObjectFactory domainObjectFactory = domainObjectFactoryProvider.apply(raceLogResolver); + + @Override public DomainObjectFactory getDomainObjectFactory() { return domainObjectFactory; } + @Override public MongoObjectFactory getMongoObjectFactory() { return mongoObjectFactory; } + @Override public DomainFactory getBaseDomainFactory() { return domainObjectFactory.getBaseDomainFactory(); } + @Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); } + }; + }, mediaDB, windStore, gpsFixStore, null); + } + + public RacingEventServiceImpl(final DomainObjectFactory domainObjectFactory, MongoObjectFactory mongoObjectFactory, + MediaDB mediaDB, WindStore windStore, GPSFixStore gpsFixStore) { + this((final RaceLogResolver raceLogResolver)-> { + return new ConstructorParameters() { + @Override public DomainObjectFactory getDomainObjectFactory() { return domainObjectFactory; } + @Override public MongoObjectFactory getMongoObjectFactory() { return mongoObjectFactory; } + @Override public DomainFactory getBaseDomainFactory() { return domainObjectFactory.getBaseDomainFactory(); } + @Override public CompetitorStore getCompetitorStore() { return getBaseDomainFactory().getCompetitorStore(); } + }; + }, mediaDB, windStore, gpsFixStore, null); + } + + /** + * @param windStore + * if null, a default {@link MongoWindStore} will be used, based on the persistence set-up + * of this service + * @param serviceFinderFactory + * used to find the services handling specific types of tracking devices, such as the persistent storage + * of {@link DeviceIdentifier}s of specific device types or the managing of the device-to-competitor + * associations per race tracked. + */ + public RacingEventServiceImpl(Function constructorParametersProvider, MediaDB mediaDb, + WindStore windStore, GPSFixStore gpsFixStore, TypeBasedServiceFinderFactory serviceFinderFactory) { + logger.info("Created " + this); + final ConstructorParameters constructorParameters = constructorParametersProvider.apply(this); + this.domainObjectFactory = constructorParameters.getDomainObjectFactory(); + this.masterDataClassLoaders.add(this.getClass().getClassLoader()); + joinedClassLoader = new JoinedClassLoader(masterDataClassLoaders); + this.operationsSentToMasterForReplication = new HashSet<>(); + this.baseDomainFactory = constructorParameters.getBaseDomainFactory(); + this.mongoObjectFactory = constructorParameters.getMongoObjectFactory(); + this.mediaDB = mediaDb; + this.competitorStore = constructorParameters.getCompetitorStore(); + if (windStore == null) { + try { + windStore = MongoWindStoreFactory.INSTANCE.getMongoWindStore(mongoObjectFactory, domainObjectFactory); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + this.competitorStore.addCompetitorUpdateListener(new CompetitorUpdateListener() { + @Override + public void competitorUpdated(Competitor competitor) { + replicate(new UpdateCompetitor(competitor.getId().toString(), competitor.getName(), competitor + .getColor(), competitor.getEmail(), competitor.getBoat().getSailID(), competitor.getTeam().getNationality(), + competitor.getTeam().getImage(), competitor.getFlagImage(), + competitor.getTimeOnTimeFactor(), competitor.getTimeOnDistanceAllowancePerNauticalMile())); + } + }); + this.windStore = windStore; + this.dataImportLock = new DataImportLockWithProgress(); + + remoteSailingServerSet = new RemoteSailingServerSet(scheduler); + regattasByName = new ConcurrentHashMap(); + regattasByNameLock = new NamedReentrantReadWriteLock("regattasByName for " + this, /* fair */false); + eventsById = new ConcurrentHashMap(); + regattaTrackingCache = new ConcurrentHashMap<>(); + regattaTrackingCacheLock = new NamedReentrantReadWriteLock("regattaTrackingCache for " + this, /* fair */false); + raceTrackersByRegatta = new ConcurrentHashMap<>(); + raceTrackersByRegattaLock = new NamedReentrantReadWriteLock("raceTrackersByRegatta for " + this, /* fair */ + false); + raceTrackersByID = new ConcurrentHashMap<>(); + raceTrackersByIDLocks = new ConcurrentHashMap<>(); + leaderboardGroupsByName = new ConcurrentHashMap<>(); + leaderboardGroupsByID = new ConcurrentHashMap<>(); + leaderboardGroupsByNameLock = new NamedReentrantReadWriteLock("leaderboardGroupsByName for " + this, /* fair */ + false); + leaderboardsByName = new ConcurrentHashMap(); + leaderboardsByNameLock = new NamedReentrantReadWriteLock("leaderboardsByName for " + this, /* fair */false); + operationExecutionListeners = new ConcurrentHashMap<>(); + courseListeners = new ConcurrentHashMap<>(); + persistentRegattasForRaceIDs = new ConcurrentHashMap<>(); + final int SIMULATION_THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors()/3, 1); + Executor simulatorExecutor = new ThreadPoolExecutor(/* corePoolSize */SIMULATION_THREAD_POOL_SIZE, + /* maximumPoolSize */SIMULATION_THREAD_POOL_SIZE, + /* keepAliveTime */60, TimeUnit.SECONDS, + /* workQueue */new LinkedBlockingQueue()); + // TODO: initialize smart-future-cache for simulation-results and add to simulation-service + simulationService = SimulationServiceFactory.INSTANCE.getService(simulatorExecutor, this); + this.raceLogReplicator = new RaceLogReplicator(this); + this.regattaLogReplicator = new RegattaLogReplicator(this); + this.raceLogScoringReplicator = new RaceLogScoringReplicator(this); + this.mediaLibrary = new MediaLibrary(); + if (gpsFixStore == null) { + try { + gpsFixStore = MongoGPSFixStoreFactory.INSTANCE.getMongoGPSFixStore(mongoObjectFactory, + domainObjectFactory, serviceFinderFactory); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + this.gpsFixStore = gpsFixStore; + this.configurationMap = new DeviceConfigurationMapImpl(); + this.serviceFinderFactory = serviceFinderFactory; + + sailingServerConfiguration = domainObjectFactory.loadServerConfiguration(); + final Iterable> loadedEventsWithRequireStoreFlag = loadStoredEvents(); + loadStoredRegattas(); + loadRaceIDToRegattaAssociations(); + loadStoredLeaderboardsAndGroups(); + loadLinksFromEventsToLeaderboardGroups(); + loadMediaLibary(); + loadStoredDeviceConfigurations(); + loadAllRemoteSailingServersAndSchedulePeriodicEventCacheRefresh(); + + // Stores all events which run through a data migration + // Remark: must be called after loadLinksFromEventsToLeaderboardGroups(), otherwise would loose the Event -> LeaderboardGroup relation + for (Pair eventAndRequireStoreFlag : loadedEventsWithRequireStoreFlag) { + if (eventAndRequireStoreFlag.getB()) { + mongoObjectFactory.storeEvent(eventAndRequireStoreFlag.getA()); + } + } + } + + @Override + public boolean isCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster() { + return currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster.get(); + } + + @Override + public void setCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster( + boolean currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster) { + this.currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster + .set(currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster); + } + + @Override + public PolarDataService getPolarDataService() { + return polarDataService; + } + + @Override + public SimulationService getSimulationService() { + return simulationService; + } + + @Override + public void clearState() throws Exception { + for (String leaderboardGroupName : new ArrayList<>(this.leaderboardGroupsByName.keySet())) { + removeLeaderboardGroup(leaderboardGroupName); + } + for (String leaderboardName : new ArrayList<>(this.leaderboardsByName.keySet())) { + removeLeaderboard(leaderboardName); + } + for (Regatta regatta : new ArrayList<>(this.regattasByName.values())) { + stopTracking(regatta); + removeRegatta(regatta); + } + for (Event event : new ArrayList<>(this.eventsById.values())) { + removeEvent(event.getId()); + } + for (MediaTrack mediaTrack : this.mediaLibrary.allTracks()) { + mediaTrackDeleted(mediaTrack); + } + // TODO clear user store? See bug 2430. + this.competitorStore.clear(); + } + + @Override + public com.sap.sailing.domain.base.DomainFactory getBaseDomainFactory() { + return baseDomainFactory; + } + + @Override + public MongoObjectFactory getMongoObjectFactory() { + return mongoObjectFactory; + } + + @Override + public DomainObjectFactory getDomainObjectFactory() { + return domainObjectFactory; + } + + private void loadRaceIDToRegattaAssociations() { + persistentRegattasForRaceIDs.putAll(domainObjectFactory.loadRaceIDToRegattaAssociations(this)); + } + + private void loadStoredRegattas() { + LockUtil.lockForWrite(regattasByNameLock); + try { + for (Regatta regatta : domainObjectFactory.loadAllRegattas(this)) { + logger.info("putting regatta " + regatta.getName() + " (" + regatta.hashCode() + + ") into regattasByName"); + regattasByName.put(regatta.getName(), regatta); + regatta.addRegattaListener(this); + onRegattaLikeAdded(regatta); + regatta.addRaceColumnListener(raceLogReplicator); + regatta.addRaceColumnListener(raceLogScoringReplicator); + } + } finally { + LockUtil.unlockAfterWrite(regattasByNameLock); + } + } + + private Iterable> loadStoredEvents() { + Iterable> loadedEventsWithRequireStoreFlag = domainObjectFactory.loadAllEvents(); + for (Pair eventAndFlag : loadedEventsWithRequireStoreFlag) { + Event event = eventAndFlag.getA(); + if (event.getId() != null) + eventsById.put(event.getId(), event); + } + return loadedEventsWithRequireStoreFlag; + } + + private void loadLinksFromEventsToLeaderboardGroups() { + domainObjectFactory.loadLeaderboardGroupLinksForEvents(/* eventResolver */this, /* leaderboardGroupResolver */ + this); + } + + private void loadAllRemoteSailingServersAndSchedulePeriodicEventCacheRefresh() { + for (RemoteSailingServerReference sailingServer : domainObjectFactory.loadAllRemoteSailingServerReferences()) { + remoteSailingServerSet.add(sailingServer); + } + } + + /** + * Collects media track references from the configured sources (mongo DB by default, ftp folder yet to be + * implemented). The method is expected to be called initially blocking the API until finished. + * + * Subsequent calls (assumed to be triggered from the admin console or in scheduled intervals) don't need to block. + * In that case, the API will simply serve the current state. + * + */ + private void loadMediaLibary() { + Collection allDbMediaTracks = mediaDB.loadAllMediaTracks(); + mediaTracksAdded(allDbMediaTracks); + } + + private void loadStoredDeviceConfigurations() { + for (Entry entry : domainObjectFactory + .loadAllDeviceConfigurations()) { + configurationMap.put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void addLeaderboard(Leaderboard leaderboard) { + LockUtil.lockForWrite(leaderboardsByNameLock); + try { + leaderboardsByName.put(leaderboard.getName(), leaderboard); + } finally { + LockUtil.unlockAfterWrite(leaderboardsByNameLock); + } + // RaceColumns of RegattaLeaderboards are tracked via its Regatta! + if (leaderboard instanceof FlexibleLeaderboard) { + onRegattaLikeAdded(((FlexibleLeaderboard) leaderboard).getRegattaLike()); + leaderboard.addRaceColumnListener(raceLogReplicator); + leaderboard.addRaceColumnListener(raceLogScoringReplicator); + } + } + + private void loadStoredLeaderboardsAndGroups() { + logger.info("loading stored leaderboards and groups"); + // Loading all leaderboard groups and the contained leaderboards + for (LeaderboardGroup leaderboardGroup : domainObjectFactory.getAllLeaderboardGroups(this, this)) { + logger.info("loaded leaderboard group " + leaderboardGroup.getName() + " into " + this); + LockUtil.lockForWrite(leaderboardGroupsByNameLock); + try { + leaderboardGroupsByName.put(leaderboardGroup.getName(), leaderboardGroup); + leaderboardGroupsByID.put(leaderboardGroup.getId(), leaderboardGroup); + } finally { + LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock); + } + } + // Loading the remaining leaderboards + domainObjectFactory.getLeaderboardsNotInGroup(this, this); + logger.info("done with loading stored leaderboards and groups"); + } + + @Override + public FlexibleLeaderboard addFlexibleLeaderboard(String leaderboardName, String leaderboardDisplayName, + int[] discardThresholds, ScoringScheme scoringScheme, Serializable courseAreaId) { + logger.info("adding flexible leaderboard " + leaderboardName); + CourseArea courseArea = getCourseArea(courseAreaId); + FlexibleLeaderboard result = new FlexibleLeaderboardImpl(getRaceLogStore(), getRegattaLogStore(), + leaderboardName, new ThresholdBasedResultDiscardingRuleImpl(discardThresholds), scoringScheme, + courseArea); + result.setDisplayName(leaderboardDisplayName); + if (getLeaderboardByName(leaderboardName) != null) { + throw new IllegalArgumentException("Leaderboard with name " + leaderboardName + " already exists"); + } + addLeaderboard(result); + mongoObjectFactory.storeLeaderboard(result); + return result; + } + + @Override + public CourseArea getCourseArea(Serializable courseAreaId) { + for (Event event : getAllEvents()) { + for (CourseArea courseArea : event.getVenue().getCourseAreas()) { + if (courseArea.getId().equals(courseAreaId)) { + return courseArea; + } + } + } + return null; + } + + @Override + public RegattaLeaderboard addRegattaLeaderboard(RegattaIdentifier regattaIdentifier, String leaderboardDisplayName, + int[] discardThresholds) { + Regatta regatta = getRegatta(regattaIdentifier); + logger.info("adding regatta leaderboard for regatta " + + (regatta == null ? "null" : (regatta.getName() + " (" + regatta.hashCode() + ")")) + " to " + this); + RegattaLeaderboard result = null; + if (regatta != null) { + result = new RegattaLeaderboardImpl(regatta, new ThresholdBasedResultDiscardingRuleImpl(discardThresholds)); + result.setDisplayName(leaderboardDisplayName); + if (getLeaderboardByName(result.getName()) != null) { + throw new IllegalArgumentException("Leaderboard with name " + result.getName() + " already exists in " + + this); + } + addLeaderboard(result); + mongoObjectFactory.storeLeaderboard(result); + } else { + logger.warning("Cannot find regatta " + regattaIdentifier + + ". Hence, cannot create regatta leaderboard for it."); + } + return result; + } + + @Override + public RaceColumn addColumnToLeaderboard(String columnName, String leaderboardName, boolean medalRace) { + Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + if (leaderboard != null) { + if (leaderboard instanceof FlexibleLeaderboard) { + // uses the default fleet as the single fleet for the new column + RaceColumn result = ((FlexibleLeaderboard) leaderboard).addRaceColumn(columnName, medalRace); + updateStoredLeaderboard((FlexibleLeaderboard) leaderboard); + return result; + } else { + throw new IllegalArgumentException("Leaderboard named " + leaderboardName + + " is not a FlexibleLeaderboard"); + } + } else { + throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found"); + } + } + + @Override + public void moveLeaderboardColumnUp(String leaderboardName, String columnName) { + Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + if (leaderboard != null && leaderboard instanceof FlexibleLeaderboard) { + ((FlexibleLeaderboard) leaderboard).moveRaceColumnUp(columnName); + updateStoredLeaderboard((FlexibleLeaderboard) leaderboard); + } else { + throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found"); + } + } + + @Override + public void moveLeaderboardColumnDown(String leaderboardName, String columnName) { + Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + if (leaderboard != null && leaderboard instanceof FlexibleLeaderboard) { + ((FlexibleLeaderboard) leaderboard).moveRaceColumnDown(columnName); + updateStoredLeaderboard((FlexibleLeaderboard) leaderboard); + } else { + throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found"); + } + } + + @Override + public void removeLeaderboardColumn(String leaderboardName, String columnName) { + Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + if (leaderboard == null) { + throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found"); + } else if (!(leaderboard instanceof FlexibleLeaderboard)) { + throw new IllegalArgumentException("Columns cannot be removed from Leaderboard named " + leaderboardName); + } else { + ((FlexibleLeaderboard) leaderboard).removeRaceColumn(columnName); + updateStoredLeaderboard((FlexibleLeaderboard) leaderboard); + } + } + + @Override + public void renameLeaderboardColumn(String leaderboardName, String oldColumnName, String newColumnName) { + Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + if (leaderboard != null) { + final RaceColumn raceColumn = leaderboard.getRaceColumnByName(oldColumnName); + if (raceColumn instanceof FlexibleRaceColumn) { + // remove race log under old identifier; the race log identifier changes + for (Fleet fleet : raceColumn.getFleets()) { + getMongoObjectFactory().removeRaceLog(raceColumn.getRaceLogIdentifier(fleet)); + } + ((FlexibleRaceColumn) raceColumn).setName(newColumnName); + // store the race logs again under the new identifiers + storeRaceLogs(raceColumn); + updateStoredLeaderboard(leaderboard); + } else { + throw new IllegalArgumentException("Race column " + oldColumnName + " cannot be renamed"); + } + } else { + throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found"); + } + } + + /** + * When a race column is renamed, its race log identifiers change. Therefore, the race logs need to be stored under + * the new identifier again to be consistent with the in-memory image again. + */ + private void storeRaceLogs(RaceColumn raceColumn) { + for (Fleet fleet : raceColumn.getFleets()) { + RaceLogIdentifier identifier = raceColumn.getRaceLogIdentifier(fleet); + RaceLogEventVisitor storeVisitor = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStoreVisitor( + identifier, getMongoObjectFactory()); + RaceLog raceLog = raceColumn.getRaceLog(fleet); + raceLog.lockForRead(); + try { + for (RaceLogEvent e : raceLog.getRawFixes()) { + e.accept(storeVisitor); + } + } finally { + raceLog.unlockAfterRead(); + } + } + } + + @Override + public void updateLeaderboardColumnFactor(String leaderboardName, String columnName, Double factor) { + Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + if (leaderboard != null) { + final RaceColumn raceColumn = leaderboard.getRaceColumnByName(columnName); + if (raceColumn != null) { + raceColumn.setFactor(factor); + updateStoredLeaderboard(leaderboard); + } else { + throw new IllegalArgumentException("Race column " + columnName + " not found in leaderboard " + + leaderboardName); + } + } else { + throw new IllegalArgumentException("Leaderboard named " + leaderboardName + " not found"); + } + } + + @Override + public void renameLeaderboard(String oldName, String newName) { + final Leaderboard toRename = leaderboardsByName.get(oldName); + LockUtil.lockForWrite(leaderboardsByNameLock); + try { + if (toRename == null) { + throw new IllegalArgumentException("No leaderboard with name " + oldName + " found"); + } + if (leaderboardsByName.containsKey(newName)) { + throw new IllegalArgumentException("Leaderboard with name " + newName + " already exists"); + } + if (toRename instanceof Renamable) { + ((Renamable) toRename).setName(newName); + leaderboardsByName.remove(oldName); + leaderboardsByName.put(newName, toRename); + } else { + throw new IllegalArgumentException("Leaderboard with name " + newName + " is of type " + + toRename.getClass().getSimpleName() + " and therefore cannot be renamed"); + } + } finally { + LockUtil.unlockAfterWrite(leaderboardsByNameLock); + } + // don't need the lock anymore to update DB + if (toRename instanceof Renamable) { + mongoObjectFactory.renameLeaderboard(oldName, newName); + syncGroupsAfterLeaderboardChange(toRename, true); + } + } + + @Override + public void updateStoredLeaderboard(Leaderboard leaderboard) { + getMongoObjectFactory().storeLeaderboard(leaderboard); + syncGroupsAfterLeaderboardChange(leaderboard, true); + } + + @Override + public void updateStoredRegatta(Regatta regatta) { + if (regatta.isPersistent()) { + mongoObjectFactory.storeRegatta(regatta); + } + } + + /** + * Checks all groups, if they contain a leaderboard with the name of the updatedLeaderboard and + * replaces the one in the group with the updated one.
    + * This synchronizes things like the RaceIdentifier in the leaderboard columns. + */ + private void syncGroupsAfterLeaderboardChange(Leaderboard updatedLeaderboard, boolean doDatabaseUpdate) { + boolean groupNeedsUpdate = false; + for (LeaderboardGroup leaderboardGroup : leaderboardGroupsByName.values()) { + for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { + if (leaderboard == updatedLeaderboard) { + int index = leaderboardGroup.getIndexOf(leaderboard); + leaderboardGroup.removeLeaderboard(leaderboard); + leaderboardGroup.addLeaderboardAt(updatedLeaderboard, index); + groupNeedsUpdate = true; + // TODO we assume that the leaderboard names are unique, so we can break the inner loop here + break; + } + } + + if (doDatabaseUpdate && groupNeedsUpdate) { + mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup); + } + groupNeedsUpdate = false; + } + } + + @Override + public void removeLeaderboard(String leaderboardName) { + Leaderboard leaderboard = removeLeaderboardFromLeaderboardsByName(leaderboardName); + if (leaderboard != null) { + leaderboard.removeRaceColumnListener(raceLogReplicator); + leaderboard.removeRaceColumnListener(raceLogScoringReplicator); + mongoObjectFactory.removeLeaderboard(leaderboardName); + syncGroupsAfterLeaderboardRemove(leaderboardName, true); + if (leaderboard instanceof FlexibleLeaderboard) { + onRegattaLikeRemoved(((FlexibleLeaderboard) leaderboard).getRegattaLike()); + } + leaderboard.destroy(); + } + } + + private Leaderboard removeLeaderboardFromLeaderboardsByName(String leaderboardName) { + LockUtil.lockForWrite(leaderboardsByNameLock); + try { + return leaderboardsByName.remove(leaderboardName); + } finally { + LockUtil.unlockAfterWrite(leaderboardsByNameLock); + } + } + + /** + * Checks all groups, if they contain a leaderboard with the removedLeaderboardName and removes it from + * the group. + * + * @param removedLeaderboardName + */ + private void syncGroupsAfterLeaderboardRemove(String removedLeaderboardName, boolean doDatabaseUpdate) { + boolean groupNeedsUpdate = false; + for (LeaderboardGroup leaderboardGroup : leaderboardGroupsByName.values()) { + for (Leaderboard leaderboard : leaderboardGroup.getLeaderboards()) { + if (leaderboard.getName().equals(removedLeaderboardName)) { + leaderboardGroup.removeLeaderboard(leaderboard); + groupNeedsUpdate = true; + // TODO we assume that the leaderboard names are unique, so we can break the inner loop here + break; + } + } + + if (doDatabaseUpdate && groupNeedsUpdate) { + mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup); + } + groupNeedsUpdate = false; + } + } + + @Override + public Leaderboard getLeaderboardByName(String name) { + return leaderboardsByName.get(name); + } + + @Override + public Position getMarkPosition(Mark mark, LeaderboardThatHasRegattaLike leaderboard, TimePoint timePoint, RaceLog raceLog) { + GPSFixTrack track = null; + // If no spanning track is found, the fix closest to the time point requested is used instead + GPSFix nonSpanningFallback = null; + for (TrackedRace trackedRace : leaderboard.getTrackedRaces()) { + GPSFixTrack trackCandidate = trackedRace.getTrack(mark); + if (trackCandidate != null) { + if (spansTimePoint(trackCandidate, timePoint)) { + track = trackCandidate; + break; + } else { + nonSpanningFallback = improveTimewiseClosestFix(nonSpanningFallback, trackCandidate, timePoint); + } + } + } + if (track == null) { // no spanning track found in any tracked race, or no tracked races found + // try to load from store + DynamicGPSFixTrackImpl loadedTrack = new DynamicGPSFixTrackImpl(mark, 0); + track = loadedTrack; + Set> logs = new HashSet<>(); + logs.add(leaderboard.getRegattaLike().getRegattaLog()); + if (raceLog == null) { // no race log explicitly provided --> use all race logs + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + logs.add(raceColumn.getRaceLog(fleet)); + } + } + } else { + logs.add(raceLog); + } + for (AbstractLog log : logs) { + try { + getGPSFixStore().loadMarkTrack(loadedTrack, log, mark); + } catch (Exception e) { + logger.log(Level.WARNING, "Couldn't load mark track for mark " + mark + " from log " + log, e); + } + } + } + Position result = track.getEstimatedPosition(timePoint, /* extrapolate */ false); + if (result == null) { + result = nonSpanningFallback == null ? null : nonSpanningFallback.getPosition(); + } + return result; + } + + private GPSFix improveTimewiseClosestFix(GPSFix nonSpanningFallback, GPSFixTrack track, final TimePoint timePoint) { + GPSFix lastAtOrBefore = track.getLastFixAtOrBefore(timePoint); + GPSFix firstAtOrAfter = track.getFirstFixAtOrAfter(timePoint); + // find the fix closes to timePoint, sorting null values to the end and fixes near timePoint to the beginning + final List list = Arrays.asList(nonSpanningFallback, lastAtOrBefore, firstAtOrAfter); + list.sort(new Comparator() { + @Override + public int compare(GPSFix o1, GPSFix o2) { + final int result; + if (o1 == null) { + if (o2 == null) { + result = 0; + } else { + result = 1; + } + } else if (o2 == null) { + result = -1; + } else { + result = new Long(Math.abs(o1.getTimePoint().until(timePoint).asMillis())).compareTo( + Math.abs(o2.getTimePoint().until(timePoint).asMillis())); + } + return result; + } + }); + return list.get(0); + } + + private boolean spansTimePoint(GPSFixTrack track, TimePoint timePoint) { + return track.getLastFixAtOrBefore(timePoint) != null && track.getFirstFixAtOrAfter(timePoint) != null; + } + + @Override + public Map getLeaderboards() { + return Collections.unmodifiableMap(new HashMap(leaderboardsByName)); + } + + @Override + public SailingServerConfiguration getSailingServerConfiguration() { + return sailingServerConfiguration; + } + + @Override + public void updateServerConfiguration(SailingServerConfiguration serverConfiguration) { + this.sailingServerConfiguration = serverConfiguration; + mongoObjectFactory.storeServerConfiguration(serverConfiguration); + } + + @Override + public Map, Exception>> getPublicEventsOfAllSailingServers() { + return remoteSailingServerSet.getCachedEventsForRemoteSailingServers(); // FIXME should probably add our own + // stuff here... Is it enough to pass on + // the remote reference URL to the + // client for leaderboard group URL + // construction? + } + + @Override + public RemoteSailingServerReference addRemoteSailingServerReference(String name, URL url) { + RemoteSailingServerReference result = new RemoteSailingServerReferenceImpl(name, url); + remoteSailingServerSet.add(result); + mongoObjectFactory.storeSailingServer(result); + return result; + } + + @Override + public Iterable getLiveRemoteServerReferences() { + return remoteSailingServerSet.getLiveRemoteServerReferences(); + } + + @Override + public RemoteSailingServerReference getRemoteServerReferenceByName(String remoteServerReferenceName) { + return remoteSailingServerSet.getServerReferenceByName(remoteServerReferenceName); + } + + @Override + public com.sap.sse.common.Util.Pair, Exception> updateRemoteServerEventCacheSynchronously( + RemoteSailingServerReference ref) { + return remoteSailingServerSet.getEventsOrException(ref); + } + + @Override + public void removeRemoteSailingServerReference(String name) { + remoteSailingServerSet.remove(name); + mongoObjectFactory.removeSailingServer(name); + } + + @Override + public Iterable getAllEvents() { + return Collections.unmodifiableCollection(new ArrayList(eventsById.values())); + } + + @Override + public Event getEvent(Serializable id) { + return id == null ? null : eventsById.get(id); + } + + @Override + public Iterable getAllRegattas() { + return Collections.unmodifiableCollection(new ArrayList(regattasByName.values())); + } + + @Override + public boolean isRaceBeingTracked(Regatta regattaContext, RaceDefinition r) { + Set trackers = raceTrackersByRegatta.get(regattaContext); + if (trackers != null) { + for (RaceTracker tracker : trackers) { + final Set races = tracker.getRaces(); + if (races != null && races.contains(r)) { + return true; + } + } + } + return false; + } + + @Override + public Regatta getRegattaByName(String name) { + return name == null ? null : regattasByName.get(name); + } + + @Override + public Regatta getOrCreateDefaultRegatta(String name, String boatClassName, Serializable id) { + Regatta result = regattasByName.get(name); + if (result == null) { + result = new RegattaImpl(getRaceLogStore(), getRegattaLogStore(), name, getBaseDomainFactory() + .getOrCreateBoatClass(boatClassName), /* startDate */null, /* endDate */null, this, + getBaseDomainFactory().createScoringScheme(ScoringSchemeType.LOW_POINT), id, null); + logger.info("Created default regatta " + result.getName() + " (" + hashCode() + ") on " + this); + onRegattaLikeAdded(result); + cacheAndReplicateDefaultRegatta(result); + } + return result; + } + + private void onRegattaLikeAdded(IsRegattaLike isRegattaLike) { + isRegattaLike.addListener(regattaLogReplicator); + } + + private void onRegattaLikeRemoved(IsRegattaLike isRegattaLike) { + isRegattaLike.removeListener(regattaLogReplicator); + getRegattaLogStore().removeRegattaLog(isRegattaLike.getRegattaLikeIdentifier()); + } + + @Override + public Regatta createRegatta(String fullRegattaName, String boatClassName, TimePoint startDate, TimePoint endDate, + Serializable id, Iterable series, boolean persistent, ScoringScheme scoringScheme, + Serializable defaultCourseAreaId, boolean useStartTimeInference, RankingMetricConstructor rankingMetricConstructor) { + com.sap.sse.common.Util.Pair regattaWithCreatedFlag = getOrCreateRegattaWithoutReplication( + fullRegattaName, boatClassName, startDate, endDate, id, series, persistent, scoringScheme, + defaultCourseAreaId, useStartTimeInference, rankingMetricConstructor); + Regatta regatta = regattaWithCreatedFlag.getA(); + if (regattaWithCreatedFlag.getB()) { + onRegattaLikeAdded(regatta); + replicateSpecificRegattaWithoutRaceColumns(regatta); + } + return regatta; + } + + @Override + public void addRegattaWithoutReplication(Regatta regatta) { + UUID defaultCourseAreaId = null; + if (regatta.getDefaultCourseArea() != null) { + defaultCourseAreaId = regatta.getDefaultCourseArea().getId(); + } + boolean wasAdded = addAndConnectRegatta(regatta.isPersistent(), defaultCourseAreaId, regatta); + if (!wasAdded) { + logger.info("Regatta with name " + regatta.getName() + " already existed, so it hasn't been added."); + } + } + + private RaceLogStore getRaceLogStore() { + return MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore(mongoObjectFactory, domainObjectFactory); + } + + private RegattaLogStore getRegattaLogStore() { + return MongoRegattaLogStoreFactory.INSTANCE.getMongoRegattaLogStore(mongoObjectFactory, domainObjectFactory); + } + + @Override + public com.sap.sse.common.Util.Pair getOrCreateRegattaWithoutReplication(String fullRegattaName, + String boatClassName, TimePoint startDate, TimePoint endDate, Serializable id, + Iterable series, boolean persistent, ScoringScheme scoringScheme, + Serializable defaultCourseAreaId, boolean useStartTimeInference, RankingMetricConstructor rankingMetricConstructor) { + CourseArea courseArea = getCourseArea(defaultCourseAreaId); + Regatta regatta = new RegattaImpl(getRaceLogStore(), getRegattaLogStore(), fullRegattaName, + getBaseDomainFactory().getOrCreateBoatClass(boatClassName), startDate, endDate, series, persistent, + scoringScheme, id, courseArea, useStartTimeInference, rankingMetricConstructor); + boolean wasCreated = addAndConnectRegatta(persistent, defaultCourseAreaId, regatta); + if (wasCreated) { + logger.info("Created regatta " + regatta.getName() + " (" + hashCode() + ") on " + this); + } + return new com.sap.sse.common.Util.Pair(regatta, wasCreated); + } + + private boolean addAndConnectRegatta(boolean persistent, Serializable defaultCourseAreaId, Regatta regatta) { + boolean wasCreated = false; + // try a quick read protected by the concurrent hash map implementation + if (!regattasByName.containsKey(regatta.getName())) { + LockUtil.lockForWrite(regattasByNameLock); + try { + // check again, now that we hold the exclusive write lock + if (!regattasByName.containsKey(regatta.getName())) { + wasCreated = true; + logger.info("putting regatta " + regatta.getName() + " (" + regatta.hashCode() + + ") into regattasByName of " + this); + regattasByName.put(regatta.getName(), regatta); + regatta.addRegattaListener(this); + regatta.addRaceColumnListener(raceLogReplicator); + regatta.addRaceColumnListener(raceLogScoringReplicator); + } + } finally { + LockUtil.unlockAfterWrite(regattasByNameLock); + } + } + if (persistent) { + updateStoredRegatta(regatta); + } + for (Event event : getAllEvents()) { + for (CourseArea eventCourseArea : event.getVenue().getCourseAreas()) { + if (defaultCourseAreaId != null && eventCourseArea.getId().equals(defaultCourseAreaId)) { + event.addRegatta(regatta); + } + } + } + return wasCreated; + } + + @Override + public void addRace(RegattaIdentifier addToRegatta, RaceDefinition raceDefinition) { + Regatta regatta = getRegatta(addToRegatta); + regatta.addRace(raceDefinition); // will trigger the raceAdded operation because this service is listening on + // all its regattas + } + + /** + * If the regatta {@link Regatta#isPersistent() is a persistent one}, the association of the race with + * the regatta is remembered persistently so that {@link #getRememberedRegattaForRace(Serializable)} will provide + * it. + */ + @Override + public void raceAdded(Regatta regatta, RaceDefinition raceDefinition) { + if (regatta.isPersistent()) { + setRegattaForRace(regatta, raceDefinition); + } + final CourseChangeReplicator listener = new CourseChangeReplicator(this, regatta, raceDefinition); + courseListeners.put(raceDefinition, listener); + raceDefinition.getCourse().addCourseListener(listener); + replicate(new AddRaceDefinition(regatta.getRegattaIdentifier(), raceDefinition)); + } + + @Override + public void raceRemoved(Regatta regatta, RaceDefinition raceDefinition) { + raceDefinition.getCourse().removeCourseListener(courseListeners.remove(raceDefinition)); + } + + private NamedReentrantReadWriteLock lockRaceTrackersById(Object trackerId) { + NamedReentrantReadWriteLock lock; + synchronized (raceTrackersByIDLocks) { + lock = raceTrackersByIDLocks.get(trackerId); + if (lock == null) { + lock = new NamedReentrantReadWriteLock("raceTrackersByIDLock for " + trackerId, /* fair */false); + raceTrackersByIDLocks.put(trackerId, lock); + } + } + LockUtil.lockForWrite(lock); + return lock; + } + + /** + * @param lock + * need to pass the lock obtained from {@link #lockRaceTrackersById(Object)} because a competing thread + * may already have removed the lock from the {@link #raceTrackersByIDLocks} map + */ + private void unlockRaceTrackersById(Object trackerId, NamedReentrantReadWriteLock lock) { + LockUtil.unlockAfterWrite(lock); + synchronized (raceTrackersByIDLocks) { + raceTrackersByIDLocks.remove(trackerId); + } + } + + @Override + public RaceHandle addRace(RegattaIdentifier regattaToAddTo, RaceTrackingConnectivityParameters params, + long timeoutInMilliseconds) throws Exception { + final Object trackerID = params.getTrackerID(); + NamedReentrantReadWriteLock raceTrackersByIdLock = lockRaceTrackersById(trackerID); + try { + RaceTracker tracker = raceTrackersByID.get(trackerID); + if (tracker == null) { + Regatta regatta = regattaToAddTo == null ? null : getRegatta(regattaToAddTo); + if (regatta == null) { + // create tracker and use an existing or create a default regatta + tracker = params.createRaceTracker(this, windStore, gpsFixStore, /* raceLogResolver */ this); + } else { + // use the regatta selected by the RaceIdentifier regattaToAddTo + tracker = params.createRaceTracker(regatta, this, windStore, gpsFixStore, /* raceLogResolver */ this); + assert tracker.getRegatta() == regatta; + } + LockUtil.lockForWrite(raceTrackersByRegattaLock); + try { + raceTrackersByID.put(trackerID, tracker); + Set trackers = raceTrackersByRegatta.get(tracker.getRegatta()); + if (trackers == null) { + trackers = Collections.newSetFromMap(new ConcurrentHashMap()); + raceTrackersByRegatta.put(tracker.getRegatta(), trackers); + } + trackers.add(tracker); + } finally { + LockUtil.unlockAfterWrite(raceTrackersByRegattaLock); + } + // TODO we assume here that the regatta name is unique which necessitates adding the boat class name to + // it in RegattaImpl constructor + String regattaName = tracker.getRegatta().getName(); + Regatta regattaWithName = regattasByName.get(regattaName); + // TODO we assume here that the regatta name is unique which necessitates adding the boat class name to + // it in RegattaImpl constructor + if (regattaWithName != null) { + if (regattaWithName != tracker.getRegatta()) { + if (Util.isEmpty(regattaWithName.getAllRaces())) { + // probably, tracker removed the last races from the old regatta and created a new one + LockUtil.lockForWrite(regattasByNameLock); + try { + regattasByName.remove(regattaName); + cacheAndReplicateDefaultRegatta(tracker.getRegatta()); + } finally { + LockUtil.unlockAfterWrite(regattasByNameLock); + } + } else { + throw new RuntimeException("Internal error. Two regatta objects with equal name " + + regattaName); + } + } + } else { + cacheAndReplicateDefaultRegatta(tracker.getRegatta()); + } + } else { + logger.warning("Race tracker with ID "+trackerID+" already found; not tracking twice to avoid race duplication"); + WindStore existingTrackersWindStore = tracker.getWindStore(); + if (!existingTrackersWindStore.equals(windStore)) { + logger.warning("Wind store mismatch. Requested wind store: " + windStore + + ". Wind store in use by existing tracker: " + existingTrackersWindStore); + } + GPSFixStore existingTrackersGPSFixStore = tracker.getGPSFixStore(); + if (!existingTrackersGPSFixStore.equals(gpsFixStore)) { + logger.warning("GPSFix store mismatch. Requested GPSFix store: " + gpsFixStore + + ". GPSFix store in use by existing tracker: " + existingTrackersGPSFixStore); + } + } + if (timeoutInMilliseconds != -1) { + scheduleAbortTrackerAfterInitialTimeout(tracker, timeoutInMilliseconds); + } + return tracker.getRacesHandle(); + } finally { + unlockRaceTrackersById(trackerID, raceTrackersByIdLock); + } + } + + /** + * The regatta and all its contained {@link Regatta#getAllRaces() races} are replicated to all replicas. + * + * @param regatta + * the series of this regatta must not have any {@link Series#getRaceColumns() race columns associated + * (yet)}. + */ + private void replicateSpecificRegattaWithoutRaceColumns(Regatta regatta) { + Serializable courseAreaId = null; + if (regatta.getDefaultCourseArea() != null) { + courseAreaId = regatta.getDefaultCourseArea().getId(); + } + replicate(new AddSpecificRegatta(regatta.getName(), regatta.getBoatClass() == null ? null : regatta + .getBoatClass().getName(), regatta.getStartDate(), regatta.getEndDate(), regatta.getId(), + getSeriesWithoutRaceColumnsConstructionParametersAsMap(regatta), regatta.isPersistent(), + regatta.getScoringScheme(), courseAreaId, regatta.useStartTimeInference(), regatta.getRankingMetricType())); + RegattaIdentifier regattaIdentifier = regatta.getRegattaIdentifier(); + for (RaceDefinition race : regatta.getAllRaces()) { + replicate(new AddRaceDefinition(regattaIdentifier, race)); + } + } + + private RegattaCreationParametersDTO getSeriesWithoutRaceColumnsConstructionParametersAsMap(Regatta regatta) { + LinkedHashMap result = new LinkedHashMap(); + for (Series s : regatta.getSeries()) { + assert Util.isEmpty(s.getRaceColumns()); + List fleetNamesAndOrdering = new ArrayList(); + for (Fleet f : s.getFleets()) { + fleetNamesAndOrdering.add(getBaseDomainFactory().convertToFleetDTO(f)); + } + result.put( + s.getName(), + new SeriesCreationParametersDTO(fleetNamesAndOrdering, s.isMedal(), s.isStartsWithZeroScore(), s + .isFirstColumnIsNonDiscardableCarryForward(), s.getResultDiscardingRule() == null ? null + : s.getResultDiscardingRule().getDiscardIndexResultsStartingWithHowManyRaces(), s + .hasSplitFleetContiguousScoring())); + } + return new RegattaCreationParametersDTO(result); + } + + /** + * If regatta is not yet in {@link #regattasByName}, it is added, this service is + * {@link Regatta#addRegattaListener(RegattaListener) added} as regatta listener, and the regatta and all its + * contained {@link Regatta#getAllRaces() races} are replicated to all replica. + */ + private void cacheAndReplicateDefaultRegatta(Regatta regatta) { + // try a quick read first, protected by regattasByName being a concurrent hash set + if (!regattasByName.containsKey(regatta.getName())) { + // now we need to obtain exclusive write access; in between, some other thread may have added a regatta by + // that + // name, so we need to check again: + LockUtil.lockForWrite(regattasByNameLock); + try { + if (!regattasByName.containsKey(regatta.getName())) { + logger.info("putting regatta " + regatta.getName() + " (" + regatta.hashCode() + + ") into regattasByName of " + this); + regattasByName.put(regatta.getName(), regatta); + regatta.addRegattaListener(this); + regatta.addRaceColumnListener(raceLogReplicator); + regatta.addRaceColumnListener(raceLogScoringReplicator); + + replicate(new AddDefaultRegatta(regatta.getName(), regatta.getBoatClass() == null ? null : regatta + .getBoatClass().getName(), regatta.getStartDate(), regatta.getEndDate(), regatta.getId())); + RegattaIdentifier regattaIdentifier = regatta.getRegattaIdentifier(); + for (RaceDefinition race : regatta.getAllRaces()) { + replicate(new AddRaceDefinition(regattaIdentifier, race)); + } + } + } finally { + LockUtil.unlockAfterWrite(regattasByNameLock); + } + } + } + + @Override + public DynamicTrackedRace createTrackedRace(RegattaAndRaceIdentifier raceIdentifier, WindStore windStore, + GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, + long millisecondsOverWhichToAverageSpeed, boolean useMarkPassingCalculator) { + DynamicTrackedRegatta trackedRegatta = getOrCreateTrackedRegatta(getRegatta(raceIdentifier)); + RaceDefinition race = getRace(raceIdentifier); + return trackedRegatta.createTrackedRace(race, Collections. emptyList(), windStore, gpsFixStore, + delayToLiveInMillis, millisecondsOverWhichToAverageWind, millisecondsOverWhichToAverageSpeed, + /* raceDefinitionSetToUpdate */null, useMarkPassingCalculator, /* raceLogResolver */ this); + } + + private void ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking( + DynamicTrackedRegatta trackedRegatta) { + if (regattasObservedForDefaultLeaderboard.add(trackedRegatta)) { + trackedRegatta.addRaceListener(new RaceAdditionListener()); + } + } + + private void stopObservingRegattaForRedaultLeaderboardAndAutoLeaderboardLinking(DynamicTrackedRegatta trackedRegatta) { + regattasObservedForDefaultLeaderboard.remove(trackedRegatta); + } + + /** + * A listener class used to ensure that when a tracked race is added to any {@link TrackedRegatta} managed by this + * service, the service adds the tracked race to the default leaderboard and links it to the leaderboard columns + * that were previously connected to it. Additionally, a {@link RaceChangeListener} is added to the + * {@link TrackedRace} which is responsible for triggering the replication of all relevant changes to the tracked + * race. When a tracked race is removed, the {@link TrackedRaceReplicator} that was added as listener to that + * tracked race is removed again. + * + * A {@link PolarFixCacheUpdater} is added to every race so that polar fixes are aggregated when new GPS fixes + * arrive. + * + * @author Axel Uhl (d043530) + * + */ + private class RaceAdditionListener implements RaceListener, Serializable { + private static final long serialVersionUID = 1036955460477000265L; + + private final Map trackedRaceReplicators; + + private final Map polarFixCacheUpdaters; + + public RaceAdditionListener() { + this.trackedRaceReplicators = new HashMap(); + this.polarFixCacheUpdaters = new HashMap(); + } + + @Override + public void raceRemoved(TrackedRace trackedRace) { + TrackedRaceReplicator trackedRaceReplicator = trackedRaceReplicators.remove(trackedRace); + if (trackedRaceReplicator != null) { + trackedRace.removeListener(trackedRaceReplicator); + } + PolarFixCacheUpdater polarFixCacheUpdater = polarFixCacheUpdaters.remove(trackedRace); + if (polarFixCacheUpdater != null) { + trackedRace.removeListener(polarFixCacheUpdater); + } + } + + @Override + public void raceAdded(TrackedRace trackedRace) { + // replicate the addition of the tracked race: + CreateTrackedRace op = new CreateTrackedRace(trackedRace.getRaceIdentifier(), trackedRace.getWindStore(), + trackedRace.getGPSFixStore(), trackedRace.getDelayToLiveInMillis(), + trackedRace.getMillisecondsOverWhichToAverageWind(), + trackedRace.getMillisecondsOverWhichToAverageSpeed()); + replicate(op); + linkRaceToConfiguredLeaderboardColumns(trackedRace); + TrackedRaceReplicator trackedRaceReplicator = new TrackedRaceReplicator(trackedRace); + trackedRaceReplicators.put(trackedRace, trackedRaceReplicator); + trackedRace.addListener(trackedRaceReplicator, /* fire wind already loaded */true, true); + + PolarFixCacheUpdater polarFixCacheUpdater = new PolarFixCacheUpdater(trackedRace); + polarFixCacheUpdaters.put(trackedRace, polarFixCacheUpdater); + trackedRace.addListener(polarFixCacheUpdater); + + if (polarDataService != null) { + trackedRace.setPolarDataService(polarDataService); + } + } + } + + private class PolarFixCacheUpdater extends AbstractRaceChangeListener { + + private final TrackedRace race; + + public PolarFixCacheUpdater(TrackedRace race) { + this.race = race; + } + + @Override + public void competitorPositionChanged(GPSFixMoving fix, Competitor item) { + if (polarDataService != null) { + polarDataService.competitorPositionChanged(fix, item, race); + } + } + + @Override + public void statusChanged(TrackedRaceStatus newStatus, TrackedRaceStatus oldStatus) { + if (oldStatus.getStatus() == TrackedRaceStatusEnum.LOADING + && newStatus.getStatus() != TrackedRaceStatusEnum.LOADING) { + if (polarDataService != null) { + polarDataService.raceFinishedLoading(race); + } + } + } + + } + + private class TrackedRaceReplicator implements RaceChangeListener { + private final TrackedRace trackedRace; + + public TrackedRaceReplicator(TrackedRace trackedRace) { + this.trackedRace = trackedRace; + } + + @Override + public void windSourcesToExcludeChanged(Iterable windSourcesToExclude) { + replicate(new UpdateWindSourcesToExclude(getRaceIdentifier(), windSourcesToExclude)); + } + + @Override + public void startOfTrackingChanged(TimePoint startOfTracking) { + replicate(new UpdateStartOfTracking(getRaceIdentifier(), startOfTracking)); + } + + @Override + public void endOfTrackingChanged(TimePoint endOfTracking) { + replicate(new UpdateEndOfTracking(getRaceIdentifier(), endOfTracking)); + } + + @Override + public void startTimeReceivedChanged(TimePoint startTimeReceived) { + replicate(new UpdateStartTimeReceived(getRaceIdentifier(), startTimeReceived)); + } + + @Override + public void startOfRaceChanged(TimePoint oldStartOfRace, TimePoint newStartOfRace) { + // no action required; the update signaled by this call is implicit; for explicit updates + // see raceTimesChanged(TimePoint, TimePoint, TimePoint). + } + + @Override + public void waypointAdded(int zeroBasedIndex, Waypoint waypointThatGotAdded) { + // no-op; the course change is replicated by the separate CourseChangeReplicator + } + + @Override + public void waypointRemoved(int zeroBasedIndex, Waypoint waypointThatGotRemoved) { + // no-op; the course change is replicated by the separate CourseChangeReplicator + } + + @Override + public void delayToLiveChanged(long delayToLiveInMillis) { + replicate(new UpdateRaceDelayToLive(getRaceIdentifier(), delayToLiveInMillis)); + } + + @Override + public void windDataReceived(Wind wind, WindSource windSource) { + replicate(new RecordWindFix(getRaceIdentifier(), windSource, wind)); + } + + @Override + public void windDataRemoved(Wind wind, WindSource windSource) { + replicate(new RemoveWindFix(getRaceIdentifier(), windSource, wind)); + } + + @Override + public void windAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { + replicate(new UpdateWindAveragingTime(getRaceIdentifier(), newMillisecondsOverWhichToAverage)); + } + + @Override + public void competitorPositionChanged(GPSFixMoving fix, Competitor competitor) { + replicate(new RecordCompetitorGPSFix(getRaceIdentifier(), competitor, fix)); + } + + @Override + public void statusChanged(TrackedRaceStatus newStatus, TrackedRaceStatus oldStatus) { + replicate(new UpdateTrackedRaceStatus(getRaceIdentifier(), newStatus)); + } + + @Override + public void markPositionChanged(GPSFix fix, Mark mark, boolean firstInTrack) { + final RecordMarkGPSFix operation; + if (firstInTrack) { + operation = new RecordMarkGPSFixForNewMarkTrack(getRaceIdentifier(), mark, fix); + } else { + operation = new RecordMarkGPSFixForExistingTrack(getRaceIdentifier(), mark, fix); + } + replicate(operation); + } + + @Override + public void markPassingReceived(Competitor competitor, Map oldMarkPassings, + Iterable markPassings) { + replicate(new UpdateMarkPassings(getRaceIdentifier(), competitor, markPassings)); + } + + @Override + public void speedAveragingChanged(long oldMillisecondsOverWhichToAverage, long newMillisecondsOverWhichToAverage) { + replicate(new UpdateWindAveragingTime(getRaceIdentifier(), newMillisecondsOverWhichToAverage)); + } + + private RegattaAndRaceIdentifier getRaceIdentifier() { + return trackedRace.getRaceIdentifier(); + } + + } + + /** + * Based on the trackedRace's {@link TrackedRace#getRaceIdentifier() race identifier}, the tracked race + * is (re-)associated to all {@link RaceColumn race columns} that currently have no + * {@link RaceColumn#getTrackedRace(Fleet) tracked race assigned} and whose + * {@link RaceColumn#getRaceIdentifier(Fleet) race identifier} equals that of trackedRace. + */ + private void linkRaceToConfiguredLeaderboardColumns(TrackedRace trackedRace) { + boolean leaderboardHasChanged = false; + RegattaAndRaceIdentifier trackedRaceIdentifier = trackedRace.getRaceIdentifier(); + for (Leaderboard leaderboard : getLeaderboards().values()) { + for (RaceColumn column : leaderboard.getRaceColumns()) { + for (Fleet fleet : column.getFleets()) { + if (trackedRaceIdentifier.equals(column.getRaceIdentifier(fleet)) + && column.getTrackedRace(fleet) == null) { + column.setTrackedRace(fleet, trackedRace); + leaderboardHasChanged = true; + replicate(new ConnectTrackedRaceToLeaderboardColumn(leaderboard.getName(), column.getName(), + fleet.getName(), trackedRaceIdentifier)); + } + } + } + if (leaderboardHasChanged) { + // Update the corresponding groups, to keep them in sync + syncGroupsAfterLeaderboardChange(leaderboard, /* doDatabaseUpdate */false); + } + } + } + + @Override + public void stopTracking(Regatta regatta) throws MalformedURLException, IOException, InterruptedException { + final Set trackersForRegatta = raceTrackersByRegatta.get(regatta); + if (trackersForRegatta != null) { + for (RaceTracker raceTracker : trackersForRegatta) { + final Set races = raceTracker.getRaces(); + if (races != null) { + for (RaceDefinition race : races) { + stopTrackingWind(regatta, race); + } + } + raceTracker.stop(/* preemptive */false); + final Object trackerId = raceTracker.getID(); + final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId); + try { + raceTrackersByID.remove(trackerId); + } finally { + unlockRaceTrackersById(trackerId, lock); + } + raceTrackersByID.remove(trackerId); + } + LockUtil.lockForWrite(raceTrackersByRegattaLock); + try { + raceTrackersByRegatta.remove(regatta); + } finally { + LockUtil.unlockAfterWrite(raceTrackersByRegattaLock); + } + } + } + + @Override + public void stopTrackingAndRemove(Regatta regatta) throws MalformedURLException, IOException, InterruptedException { + stopTracking(regatta); + if (regatta != null) { + if (regatta.getName() != null) { + logger.info("Removing regatta " + regatta.getName() + " (" + regatta.hashCode() + ") from " + this); + LockUtil.lockForWrite(regattasByNameLock); + try { + regattasByName.remove(regatta.getName()); + } finally { + LockUtil.unlockAfterWrite(regattasByNameLock); + } + LockUtil.lockForWrite(regattaTrackingCacheLock); + try { + regattaTrackingCache.remove(regatta); + } finally { + LockUtil.unlockAfterWrite(regattaTrackingCacheLock); + } + regatta.removeRegattaListener(this); + regatta.removeRaceColumnListener(raceLogReplicator); + regatta.removeRaceColumnListener(raceLogScoringReplicator); + } + for (RaceDefinition race : regatta.getAllRaces()) { + stopTrackingWind(regatta, race); + } + } + } + + /** + * The tracker will initially try to connect to the tracking infrastructure to obtain basic race master data. If + * this fails after some timeout, to avoid garbage and lingering threads, the task scheduled by this method will + * check after the timeout expires if race master data was successfully received. If so, the tracker continues + * normally. Otherwise, the tracker is shut down orderly by calling {@link RaceTracker#stop(boolean) stopping}. + * + * @return the scheduled task, in case the caller wants to {@link ScheduledFuture#cancel(boolean) cancel} it, e.g., + * when the tracker is stopped or has successfully received the race + */ + private ScheduledFuture scheduleAbortTrackerAfterInitialTimeout(final RaceTracker tracker, + final long timeoutInMilliseconds) { + ScheduledFuture task = getScheduler().schedule(new Runnable() { + @Override + public void run() { + if (tracker.getRaces() == null || tracker.getRaces().isEmpty()) { + try { + Regatta regatta = tracker.getRegatta(); + logger.log(Level.SEVERE, "RaceDefinition for a race in regatta " + regatta.getName() + + " not obtained within " + timeoutInMilliseconds + + "ms. Aborting tracker for this race."); + Set trackersForRegatta = raceTrackersByRegatta.get(regatta); + if (trackersForRegatta != null) { + trackersForRegatta.remove(tracker); + } + tracker.stop(/* preemptive */true); + final Object trackerId = tracker.getID(); + final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId); + try { + raceTrackersByID.remove(trackerId); + } finally { + unlockRaceTrackersById(trackerId, lock); + } + if (trackersForRegatta == null || trackersForRegatta.isEmpty()) { + stopTracking(regatta); + } + } catch (Exception e) { + logger.log(Level.SEVERE, "scheduleAbortTrackerAfterInitialTimeout", e); + e.printStackTrace(); + } + } + } + }, /* delay */timeoutInMilliseconds, /* unit */TimeUnit.MILLISECONDS); + return task; + } + + @Override + public void stopTracking(Regatta regatta, RaceDefinition race) throws MalformedURLException, IOException, + InterruptedException { + logger.info("Stopping tracking for " + race + "..."); + final Set trackerSet = raceTrackersByRegatta.get(regatta); + if (trackerSet != null) { + Iterator trackerIter = trackerSet.iterator(); + while (trackerIter.hasNext()) { + RaceTracker raceTracker = trackerIter.next(); + if (raceTracker.getRaces() != null && raceTracker.getRaces().contains(race)) { + logger.info("Found tracker to stop for races " + raceTracker.getRaces()); + raceTracker.stop(/* preemptive */false); + trackerIter.remove(); + final Object trackerId = raceTracker.getID(); + final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId); + try { + raceTrackersByID.remove(trackerId); + } finally { + unlockRaceTrackersById(trackerId, lock); + } + } + } + } else { + logger.warning("Didn't find any trackers for regatta " + regatta); + } + stopTrackingWind(regatta, race); + // if the last tracked race was removed, confirm that tracking for the entire regatta has stopped + if (trackerSet == null || trackerSet.isEmpty()) { + stopTracking(regatta); + } + } + + @Override + public void removeRegatta(Regatta regatta) throws MalformedURLException, IOException, InterruptedException { + Set leaderboardsToRemove = new HashSet<>(); + for (Leaderboard leaderboard : getLeaderboards().values()) { + if (leaderboard instanceof RegattaLeaderboard) { + RegattaLeaderboard regattaLeaderboard = (RegattaLeaderboard) leaderboard; + if (regattaLeaderboard.getRegatta() == regatta) { + leaderboardsToRemove.add(regattaLeaderboard); + } + } + } + for (RegattaLeaderboard regattaLeaderboardToRemove : leaderboardsToRemove) { + removeLeaderboard(regattaLeaderboardToRemove.getName()); + } + // avoid ConcurrentModificationException by copying the races to remove: + Set racesToRemove = new HashSet<>(); + Util.addAll(regatta.getAllRaces(), racesToRemove); + for (RaceDefinition race : racesToRemove) { + removeRace(regatta, race); + mongoObjectFactory.removeRegattaForRaceID(race.getName(), regatta); + persistentRegattasForRaceIDs.remove(race.getId().toString()); + } + if (regatta.isPersistent()) { + mongoObjectFactory.removeRegatta(regatta); + } + LockUtil.lockForWrite(regattasByNameLock); + try { + regattasByName.remove(regatta.getName()); + } finally { + LockUtil.unlockAfterWrite(regattasByNameLock); + } + regatta.removeRegattaListener(this); + regatta.removeRaceColumnListener(raceLogReplicator); + regatta.removeRaceColumnListener(raceLogScoringReplicator); + onRegattaLikeRemoved(regatta); + } + + @Override + public void removeSeries(Series series) throws MalformedURLException, IOException, InterruptedException { + Regatta regatta = series.getRegatta(); + regatta.removeSeries(series); + if (regatta.isPersistent()) { + mongoObjectFactory.storeRegatta(regatta); + } + } + + @Override + public Regatta updateRegatta(RegattaIdentifier regattaIdentifier, TimePoint startDate, TimePoint endDate, + Serializable newDefaultCourseAreaId, RegattaConfiguration newRegattaConfiguration, + Iterable series, boolean useStartTimeInference) { + // We're not doing any renaming of the regatta itself, therefore we don't have to sync on the maps. + Regatta regatta = getRegatta(regattaIdentifier); + CourseArea newCourseArea = getCourseArea(newDefaultCourseAreaId); + if (newCourseArea != regatta.getDefaultCourseArea()) { + regatta.setDefaultCourseArea(newCourseArea); + } + regatta.setStartDate(startDate); + regatta.setEndDate(endDate); + if (regatta.useStartTimeInference() != useStartTimeInference) { + regatta.setUseStartTimeInference(useStartTimeInference); + final DynamicTrackedRegatta trackedRegatta = getTrackedRegatta(regatta); + if (trackedRegatta != null) { + trackedRegatta.lockTrackedRacesForRead(); + try { + for (DynamicTrackedRace trackedRace : trackedRegatta.getTrackedRaces()) { + // the start times of the regatta's tracked races now have to be re-evaluated the next time they + // are queried + trackedRace.invalidateStartTime(); + } + } finally { + trackedRegatta.unlockTrackedRacesAfterRead(); + } + } + } + regatta.setRegattaConfiguration(newRegattaConfiguration); + if (series != null) { + for (Series seriesObj : series) { + regatta.addSeries(seriesObj); + } + } + regatta.adjustEventToRegattaAssociation(this); + if (regatta.isPersistent()) { + mongoObjectFactory.storeRegatta(regatta); + } + return regatta; + } + + @Override + public void removeRace(Regatta regatta, RaceDefinition race) throws MalformedURLException, IOException, + InterruptedException { + logger.info("Removing the race " + race + "..."); + stopAllTrackersForWhichRaceIsLastReachable(regatta, race); + stopTrackingWind(regatta, race); + TrackedRace trackedRace = getExistingTrackedRace(regatta, race); + if (trackedRace != null) { + TrackedRegatta trackedRegatta = getTrackedRegatta(regatta); + final boolean isTrackedRacesEmpty; + if (trackedRegatta != null) { + trackedRegatta.lockTrackedRacesForWrite(); + try { + trackedRegatta.removeTrackedRace(trackedRace); + isTrackedRacesEmpty = Util.isEmpty(trackedRegatta.getTrackedRaces()); + } finally { + trackedRegatta.unlockTrackedRacesAfterWrite(); + } + } else { + isTrackedRacesEmpty = false; + } + if (isTrackedRacesEmpty) { + removeTrackedRegatta(regatta); + } + // remove tracked race from RaceColumns of regatta + for (Series series : regatta.getSeries()) { + for (RaceColumnInSeries raceColumn : series.getRaceColumns()) { + for (Fleet fleet : series.getFleets()) { + if (raceColumn.getTrackedRace(fleet) == trackedRace) { + raceColumn.releaseTrackedRace(fleet); + } + } + } + } + for (Leaderboard leaderboard : getLeaderboards().values()) { + if (leaderboard instanceof FlexibleLeaderboard) { // RegattaLeaderboards have implicitly been updated by + // the code above + for (RaceColumn raceColumn : leaderboard.getRaceColumns()) { + for (Fleet fleet : raceColumn.getFleets()) { + if (raceColumn.getTrackedRace(fleet) == trackedRace) { + raceColumn.releaseTrackedRace(fleet); // but leave the RaceIdentifier on the race column + // untouched, e.g., for later re-load + } + } + } + } + } + } + // remove the race from the (default) regatta if the regatta is not persistently stored + regatta.removeRace(race); + if (!regatta.isPersistent() && Util.isEmpty(regatta.getAllRaces())) { + logger.info("Removing regatta " + regatta.getName() + " (" + regatta.hashCode() + ") from service " + this); + LockUtil.lockForWrite(regattasByNameLock); + try { + regattasByName.remove(regatta.getName()); + } finally { + LockUtil.unlockAfterWrite(regattasByNameLock); + } + regatta.removeRegattaListener(this); + regatta.removeRaceColumnListener(raceLogReplicator); + regatta.removeRaceColumnListener(raceLogScoringReplicator); + } + } + + /** + * Doesn't stop any wind trackers + */ + private void stopAllTrackersForWhichRaceIsLastReachable(Regatta regatta, RaceDefinition race) + throws MalformedURLException, IOException, InterruptedException { + if (raceTrackersByRegatta.containsKey(regatta)) { + Iterator trackerIter = raceTrackersByRegatta.get(regatta).iterator(); + while (trackerIter.hasNext()) { + RaceTracker raceTracker = trackerIter.next(); + if (raceTracker.getRaces() != null && raceTracker.getRaces().contains(race)) { + boolean foundReachableRace = false; + for (RaceDefinition raceTrackedByTracker : raceTracker.getRaces()) { + if (raceTrackedByTracker != race && isReachable(regatta, raceTrackedByTracker)) { + foundReachableRace = true; + break; + } + } + if (!foundReachableRace) { + // firstly stop the tracker + raceTracker.stop(/* preemptive */true); + // remove it from the raceTrackers by Regatta + trackerIter.remove(); + final Object trackerId = raceTracker.getID(); + final NamedReentrantReadWriteLock lock = lockRaceTrackersById(trackerId); + try { + raceTrackersByID.remove(trackerId); + } finally { + unlockRaceTrackersById(trackerId, lock); + } + // if the last tracked race was removed, remove the entire regatta + if (raceTrackersByRegatta.get(regatta).isEmpty()) { + stopTracking(regatta); + } + } + } + } + } + } + + private boolean isReachable(Regatta regatta, RaceDefinition race) { + return Util.contains(regatta.getAllRaces(), race); + } + + @Override + public void startTrackingWind(Regatta regatta, RaceDefinition race, boolean correctByDeclination) throws Exception { + for (WindTrackerFactory windTrackerFactory : getWindTrackerFactories()) { + windTrackerFactory.createWindTracker(getOrCreateTrackedRegatta(regatta), race, correctByDeclination); + } + } + + @Override + public void stopTrackingWind(Regatta regatta, RaceDefinition race) throws SocketException, IOException { + for (WindTrackerFactory windTrackerFactory : getWindTrackerFactories()) { + WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race); + if (windTracker != null) { + windTracker.stop(); + } + } + } + + @Override + public Iterable> getWindTrackedRaces() { + List> result = new ArrayList>(); + for (Regatta regatta : getAllRegattas()) { + for (RaceDefinition race : regatta.getAllRaces()) { + for (WindTrackerFactory windTrackerFactory : getWindTrackerFactories()) { + WindTracker windTracker = windTrackerFactory.getExistingWindTracker(race); + if (windTracker != null) { + result.add(new com.sap.sse.common.Util.Triple(regatta, race, + windTracker.toString())); + } + } + } + } + return result; + } + + @Override + public DynamicTrackedRace getTrackedRace(Regatta regatta, RaceDefinition race) { + return getOrCreateTrackedRegatta(regatta).getTrackedRace(race); + } + + private DynamicTrackedRace getExistingTrackedRace(Regatta regatta, RaceDefinition race) { + return getOrCreateTrackedRegatta(regatta).getExistingTrackedRace(race); + } + + @Override + public DynamicTrackedRegatta getOrCreateTrackedRegatta(Regatta regatta) { + cacheAndReplicateDefaultRegatta(regatta); + LockUtil.lockForWrite(regattaTrackingCacheLock); + try { + DynamicTrackedRegatta result = regattaTrackingCache.get(regatta); + if (result == null) { + logger.info("Creating DynamicTrackedRegattaImpl for regatta " + regatta.getName() + " with hashCode " + + regatta.hashCode()); + result = new DynamicTrackedRegattaImpl(regatta); + replicate(new TrackRegatta(regatta.getRegattaIdentifier())); + regattaTrackingCache.put(regatta, result); + ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(result); + } + return result; + } finally { + LockUtil.unlockAfterWrite(regattaTrackingCacheLock); + } + } + + @Override + public DynamicTrackedRegatta getTrackedRegatta(com.sap.sailing.domain.base.Regatta regatta) { + return regattaTrackingCache.get(regatta); + } + + @Override + public void removeTrackedRegatta(Regatta regatta) { + logger.info("Removing regatta " + regatta.getName() + " from regattaTrackingCache"); + final DynamicTrackedRegatta trackedRegatta; + LockUtil.lockForWrite(regattaTrackingCacheLock); + try { + trackedRegatta = regattaTrackingCache.remove(regatta); + } finally { + LockUtil.unlockAfterWrite(regattaTrackingCacheLock); + } + stopObservingRegattaForRedaultLeaderboardAndAutoLeaderboardLinking(trackedRegatta); + } + + @Override + public Regatta getRegatta(RegattaName regattaName) { + return (Regatta) regattasByName.get(regattaName.getRegattaName()); + } + + @Override + public Regatta getRegatta(RegattaIdentifier regattaIdentifier) { + return (Regatta) regattaIdentifier.getRegatta(this); + } + + @Override + public DynamicTrackedRace getTrackedRace(RegattaAndRaceIdentifier raceIdentifier) { + DynamicTrackedRace result = null; + Regatta regatta = regattasByName.get(raceIdentifier.getRegattaName()); + if (regatta != null) { + DynamicTrackedRegatta trackedRegatta = regattaTrackingCache.get(regatta); + if (trackedRegatta != null) { + RaceDefinition race = getRace(raceIdentifier); + if (race != null) { + result = trackedRegatta.getTrackedRace(race); + } + } + } + return result; + } + + @Override + public DynamicTrackedRace getExistingTrackedRace(RegattaAndRaceIdentifier raceIdentifier) { + Regatta regatta = getRegattaByName(raceIdentifier.getRegattaName()); + DynamicTrackedRace trackedRace = null; + if (regatta != null) { + RaceDefinition race = regatta.getRaceByName(raceIdentifier.getRaceName()); + trackedRace = getOrCreateTrackedRegatta(regatta).getExistingTrackedRace(race); + } + return trackedRace; + } + + @Override + public RaceDefinition getRace(RegattaAndRaceIdentifier regattaNameAndRaceName) { + RaceDefinition result = null; + Regatta regatta = getRegatta(regattaNameAndRaceName); + if (regatta != null) { + result = regatta.getRaceByName(regattaNameAndRaceName.getRaceName()); + } + return result; + } + + @Override + public Map getLeaderboardGroups() { + return Collections.unmodifiableMap(new HashMap(leaderboardGroupsByName)); + } + + @Override + public LeaderboardGroup getLeaderboardGroupByName(String groupName) { + return leaderboardGroupsByName.get(groupName); + } + + @Override + public LeaderboardGroup getLeaderboardGroupByID(UUID leaderboardGroupID) { + return leaderboardGroupsByID.get(leaderboardGroupID); + } + + @Override + public LeaderboardGroup addLeaderboardGroup(UUID id, String groupName, String description, String displayName, + boolean displayGroupsInReverseOrder, List leaderboardNames, + int[] overallLeaderboardDiscardThresholds, ScoringSchemeType overallLeaderboardScoringSchemeType) { + ArrayList leaderboards = new ArrayList<>(); + for (String leaderboardName : leaderboardNames) { + Leaderboard leaderboard = leaderboardsByName.get(leaderboardName); + if (leaderboard == null) { + throw new IllegalArgumentException("No leaderboard with name " + leaderboardName + " found"); + } else { + leaderboards.add(leaderboard); + } + } + LeaderboardGroup result = new LeaderboardGroupImpl(id, groupName, description, displayName, + displayGroupsInReverseOrder, leaderboards); + if (overallLeaderboardScoringSchemeType != null) { + // create overall leaderboard and its discards settings + addOverallLeaderboardToLeaderboardGroup(result, + getBaseDomainFactory().createScoringScheme(overallLeaderboardScoringSchemeType), + overallLeaderboardDiscardThresholds); + } + LockUtil.lockForWrite(leaderboardGroupsByNameLock); + try { + if (leaderboardGroupsByName.containsKey(groupName)) { + throw new IllegalArgumentException("Leaderboard group with name " + groupName + " already exists"); + } + leaderboardGroupsByName.put(groupName, result); + leaderboardGroupsByID.put(result.getId(), result); + } finally { + LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock); + } + mongoObjectFactory.storeLeaderboardGroup(result); + return result; + } + + @Override + public void addLeaderboardGroupWithoutReplication(LeaderboardGroup leaderboardGroup) { + LockUtil.lockForWrite(leaderboardGroupsByNameLock); + try { + String groupName = leaderboardGroup.getName(); + if (leaderboardGroupsByName.containsKey(groupName)) { + throw new IllegalArgumentException("Leaderboard group with name " + groupName + " already exists"); + } + leaderboardGroupsByName.put(groupName, leaderboardGroup); + leaderboardGroupsByID.put(leaderboardGroup.getId(), leaderboardGroup); + } finally { + LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock); + } + if (leaderboardGroup.hasOverallLeaderboard()) { + addLeaderboard(leaderboardGroup.getOverallLeaderboard()); + } + mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup); + } + + @Override + public void removeLeaderboardGroup(String groupName) { + final LeaderboardGroup leaderboardGroup; + LockUtil.lockForWrite(leaderboardGroupsByNameLock); + try { + leaderboardGroup = leaderboardGroupsByName.remove(groupName); + if (leaderboardGroup != null) { + leaderboardGroupsByID.remove(leaderboardGroup.getId()); + } + } finally { + LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock); + } + mongoObjectFactory.removeLeaderboardGroup(groupName); + if (leaderboardGroup != null && leaderboardGroup.getOverallLeaderboard() != null) { + removeLeaderboard(leaderboardGroup.getOverallLeaderboard().getName()); + } + } + + @Override + public void renameLeaderboardGroup(String oldName, String newName) { + LockUtil.lockForWrite(leaderboardGroupsByNameLock); + try { + final LeaderboardGroup toRename = leaderboardGroupsByName.get(oldName); + if (toRename == null) { + throw new IllegalArgumentException("No leaderboard group with name " + oldName + " found"); + } + if (leaderboardGroupsByName.containsKey(newName)) { + throw new IllegalArgumentException("Leaderboard group with name " + newName + " already exists"); + } + leaderboardGroupsByName.remove(oldName); + toRename.setName(newName); + leaderboardGroupsByName.put(newName, toRename); + } finally { + LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock); + } + mongoObjectFactory.renameLeaderboardGroup(oldName, newName); + } + + @Override + public void updateLeaderboardGroup(String oldName, String newName, String description, String displayName, + List leaderboardNames, int[] overallLeaderboardDiscardThresholds, + ScoringSchemeType overallLeaderboardScoringSchemeType) { + if (!oldName.equals(newName)) { + renameLeaderboardGroup(oldName, newName); + } + LeaderboardGroup group = getLeaderboardGroupByName(newName); + if (!description.equals(group.getDescription())) { + group.setDescriptiom(description); + } + if (!Util.equalsWithNull(displayName, group.getDisplayName())) { + group.setDisplayName(displayName); + } + group.clearLeaderboards(); + for (String leaderboardName : leaderboardNames) { + Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + if (leaderboard != null) { + group.addLeaderboard(leaderboard); + } + } + Leaderboard overallLeaderboard = group.getOverallLeaderboard(); + if (overallLeaderboard != null) { + if (overallLeaderboardScoringSchemeType == null) { + group.setOverallLeaderboard(null); + removeLeaderboard(overallLeaderboard.getName()); + } else { + // update existing overall leaderboard's discards settings; scoring scheme cannot be updated in-place + overallLeaderboard.setCrossLeaderboardResultDiscardingRule(new ThresholdBasedResultDiscardingRuleImpl( + overallLeaderboardDiscardThresholds)); + updateStoredLeaderboard(overallLeaderboard); + } + } else if (overallLeaderboard == null && overallLeaderboardScoringSchemeType != null) { + addOverallLeaderboardToLeaderboardGroup(group, + getBaseDomainFactory().createScoringScheme(overallLeaderboardScoringSchemeType), + overallLeaderboardDiscardThresholds); + } + updateStoredLeaderboardGroup(group); + } + + private void addOverallLeaderboardToLeaderboardGroup(LeaderboardGroup leaderboardGroup, + ScoringScheme scoringScheme, int[] discardThresholds) { + Leaderboard overallLeaderboard = new LeaderboardGroupMetaLeaderboard(leaderboardGroup, scoringScheme, + new ThresholdBasedResultDiscardingRuleImpl(discardThresholds)); + leaderboardGroup.setOverallLeaderboard(overallLeaderboard); + addLeaderboard(overallLeaderboard); + updateStoredLeaderboard(overallLeaderboard); + } + + @Override + public void updateStoredLeaderboardGroup(LeaderboardGroup leaderboardGroup) { + mongoObjectFactory.storeLeaderboardGroup(leaderboardGroup); + } + + private ScheduledExecutorService getScheduler() { + return scheduler; + } + + @Override + public ObjectInputStream createObjectInputStreamResolvingAgainstCache(InputStream is) throws IOException { + return getBaseDomainFactory().createObjectInputStreamResolvingAgainstThisFactory(is); + } + + @Override + public ClassLoader getDeserializationClassLoader() { + return joinedClassLoader; + } + + @Override + public Serializable getId() { + return getClass().getName(); + } + + @Override + public Iterable> getOperationExecutionListeners() { + return operationExecutionListeners.keySet(); + } + + @Override + public void addOperationExecutionListener(OperationExecutionListener listener) { + operationExecutionListeners.put(listener, listener); + } + + @Override + public void removeOperationExecutionListener(OperationExecutionListener listener) { + operationExecutionListeners.remove(listener); + } + + @Override + public void serializeForInitialReplicationInternal(ObjectOutputStream oos) throws IOException { + StringBuffer logoutput = new StringBuffer(); + + logger.info("Serializing regattas..."); + oos.writeObject(regattasByName); + logoutput.append("Serialized " + regattasByName.size() + " regattas\n"); + for (Regatta regatta : regattasByName.values()) { + logoutput.append(String.format("%3s\n", regatta.toString())); + } + + logger.info("Serializing events..."); + oos.writeObject(eventsById); + logoutput.append("\nSerialized " + eventsById.size() + " events\n"); + for (Event event : eventsById.values()) { + logoutput.append(String.format("%3s\n", event.toString())); + } + + logger.info("Serializing regattas observed..."); + oos.writeObject(regattasObservedForDefaultLeaderboard); + logger.info("Serializing regatta tracking cache..."); + oos.writeObject(regattaTrackingCache); + logger.info("Serializing leaderboard groups..."); + oos.writeObject(leaderboardGroupsByName); + logoutput.append("Serialized " + leaderboardGroupsByName.size() + " leaderboard groups\n"); + for (LeaderboardGroup lg : leaderboardGroupsByName.values()) { + logoutput.append(String.format("%3s\n", lg.toString())); + } + logger.info("Serializing leaderboards..."); + oos.writeObject(leaderboardsByName); + logoutput.append("Serialized " + leaderboardsByName.size() + " leaderboards\n"); + for (Leaderboard lg : leaderboardsByName.values()) { + logoutput.append(String.format("%3s\n", lg.toString())); + } + logger.info("Serializing media library..."); + mediaLibrary.serialize(oos); + logoutput.append("Serialized " + mediaLibrary.allTracks().size() + " media tracks\n"); + for (MediaTrack lg : mediaLibrary.allTracks()) { + logoutput.append(String.format("%3s\n", lg.toString())); + } + logger.info("Serializing persisted competitors..."); + oos.writeObject(competitorStore); + logoutput.append("Serialized " + competitorStore.size() + " persisted competitors\n"); + + logger.info("Serializing configuration map..."); + oos.writeObject(configurationMap); + logoutput.append("Serialized " + configurationMap.size() + " configuration entries\n"); + for (DeviceConfigurationMatcher matcher : configurationMap.keySet()) { + logoutput.append(String.format("%3s\n", matcher.toString())); + } + + logger.info("Serializing remote sailing server references..."); + final ArrayList remoteServerReferences = new ArrayList<>(remoteSailingServerSet + .getCachedEventsForRemoteSailingServers().keySet()); + oos.writeObject(remoteServerReferences); + logoutput.append("Serialized " + remoteServerReferences.size() + " remote sailing server references\n"); + + logger.info(logoutput.toString()); + } + + @SuppressWarnings("unchecked") + // all the casts of ois.readObject()'s return value to Map<..., ...> + // the type-parameters in the casts of the de-serialized collection objects can't be checked + @Override + public void initiallyFillFromInternal(ObjectInputStream ois) throws IOException, ClassNotFoundException, + InterruptedException { + logger.info("Performing initial replication load on " + this); + // Use this object's class's class loader as the context class loader which will then be used for + // de-serialization; this will cause all classes to be visible that this bundle + // (com.sap.sailing.server) can see + StringBuffer logoutput = new StringBuffer(); + logger.info("Reading all regattas..."); + regattasByName.putAll((Map) ois.readObject()); + logoutput.append("Received " + regattasByName.size() + " NEW regattas\n"); + for (Regatta regatta : regattasByName.values()) { + logoutput.append(String.format("%3s\n", regatta.toString())); + } + + logger.info("Reading all events..."); + eventsById.putAll((Map) ois.readObject()); + logoutput.append("\nReceived " + eventsById.size() + " NEW events\n"); + for (Event event : eventsById.values()) { + logoutput.append(String.format("%3s\n", event.toString())); + } + + // it is important that the leaderboards and tracked regattas are cleared before auto-linking to + // old leaderboards takes place which then don't match the new ones + logger.info("Reading all dynamic tracked regattas..."); + for (DynamicTrackedRegatta trackedRegattaToObserve : (Set) ois.readObject()) { + ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(trackedRegattaToObserve); + } + + logger.info("Reading all of the regatta tracking cache..."); + regattaTrackingCache.putAll((Map) ois.readObject()); + logoutput.append("Received " + regattaTrackingCache.size() + " NEW regatta tracking cache entries\n"); + + logger.info("Reading leaderboard groups..."); + leaderboardGroupsByName.putAll((Map) ois.readObject()); + logoutput.append("Received " + leaderboardGroupsByName.size() + " NEW leaderboard groups\n"); + for (LeaderboardGroup lg : leaderboardGroupsByName.values()) { + leaderboardGroupsByID.put(lg.getId(), lg); + logoutput.append(String.format("%3s\n", lg.toString())); + } + + logger.info("Reading leaderboards by name..."); + leaderboardsByName.putAll((Map) ois.readObject()); + logoutput.append("Received " + leaderboardsByName.size() + " NEW leaderboards\n"); + for (Leaderboard leaderboard : leaderboardsByName.values()) { + logoutput.append(String.format("%3s\n", leaderboard.toString())); + } + + // now fix ScoreCorrectionListener setup for LeaderboardGroupMetaLeaderboard instances: + for (Leaderboard leaderboard : leaderboardsByName.values()) { + if (leaderboard instanceof LeaderboardGroupMetaLeaderboard) { + ((LeaderboardGroupMetaLeaderboard) leaderboard) + .registerAsScoreCorrectionChangeForwarderAndRaceColumnListenerOnAllLeaderboards(); + } else if (leaderboard instanceof FlexibleLeaderboard) { + // and re-establish the RaceLogReplicator as listener on FlexibleLeaderboard objects + leaderboard.addRaceColumnListener(raceLogReplicator); + } + } + + logger.info("Reading media library..."); + mediaLibrary.deserialize(ois); + logoutput.append("Received " + mediaLibrary.allTracks().size() + " NEW media tracks\n"); + for (MediaTrack mediatrack : mediaLibrary.allTracks()) { + logoutput.append(String.format("%3s\n", mediatrack.toString())); + } + + // only copy the competitors from the deserialized competitor store; don't use it because it will have set + // a default Mongo object factory + logger.info("Reading competitors..."); + for (Competitor competitor : ((CompetitorStore) ois.readObject()).getCompetitors()) { + DynamicCompetitor dynamicCompetitor = (DynamicCompetitor) competitor; + // the following should actually be redundant because during de-serialization the Competitor objects, + // whose classes implement IsManagedByCache, should already have been got/created from/in the + // competitor store + competitorStore.getOrCreateCompetitor(dynamicCompetitor.getId(), dynamicCompetitor.getName(), + dynamicCompetitor.getColor(), dynamicCompetitor.getEmail(), dynamicCompetitor.getFlagImage(), + dynamicCompetitor.getTeam(), dynamicCompetitor.getBoat(), dynamicCompetitor.getTimeOnTimeFactor(), + dynamicCompetitor.getTimeOnDistanceAllowancePerNauticalMile()); + } + logoutput.append("Received " + competitorStore.size() + " NEW competitors\n"); + + logger.info("Reading device configurations..."); + configurationMap.putAll((DeviceConfigurationMapImpl) ois.readObject()); + logoutput.append("Received " + configurationMap.size() + " NEW configuration entries\n"); + for (DeviceConfigurationMatcher matcher : configurationMap.keySet()) { + logoutput.append(String.format("%3s\n", matcher.toString())); + } + + logger.info("Reading remote sailing server references..."); + for (RemoteSailingServerReference remoteSailingServerReference : (Iterable) ois + .readObject()) { + remoteSailingServerSet.add(remoteSailingServerReference); + logoutput.append("Received remote sailing server reference " + remoteSailingServerReference); + } + + // make sure to initialize listeners correctly + for (Regatta regatta : regattasByName.values()) { + RegattaImpl regattaImpl = (RegattaImpl) regatta; + regattaImpl.initializeSeriesAfterDeserialize(); + regattaImpl.addRaceColumnListener(raceLogReplicator); + } + // re-establish RaceLogResolver references to this RacingEventService in all TrackedRace instances + for (DynamicTrackedRegatta trackedRegatta : regattaTrackingCache.values()) { + trackedRegatta.lockTrackedRacesForRead(); + try { + for (TrackedRace trackedRace : trackedRegatta.getTrackedRaces()) { + ((TrackedRaceImpl) trackedRace).setRaceLogResolver(this); + } + } finally { + trackedRegatta.unlockTrackedRacesAfterRead(); + } + } + logger.info(logoutput.toString()); + } + + @Override + public void clearReplicaState() throws MalformedURLException, IOException, InterruptedException { + logger.info("Clearing all data structures..."); + LockUtil.lockForWrite(regattasByNameLock); + try { + regattasByName.clear(); + } finally { + LockUtil.unlockAfterWrite(regattasByNameLock); + } + regattasObservedForDefaultLeaderboard.clear(); + + if (raceTrackersByRegatta != null && !raceTrackersByRegatta.isEmpty()) { + for (DynamicTrackedRegatta regatta : regattaTrackingCache.values()) { + final Set trackers = raceTrackersByRegatta.get(regatta.getRegatta()); + if (trackers != null) { + for (RaceTracker tracker : trackers) { + tracker.stop(/* preemptive */true); + } + } + } + } + LockUtil.lockForWrite(regattaTrackingCacheLock); + try { + regattaTrackingCache.clear(); + } finally { + LockUtil.unlockAfterWrite(regattaTrackingCacheLock); + } + LockUtil.lockForWrite(raceTrackersByRegattaLock); + try { + raceTrackersByRegatta.clear(); + } finally { + LockUtil.unlockAfterWrite(raceTrackersByRegattaLock); + } + LockUtil.lockForWrite(leaderboardGroupsByNameLock); + try { + leaderboardGroupsByName.clear(); + leaderboardGroupsByID.clear(); + } finally { + LockUtil.unlockAfterWrite(leaderboardGroupsByNameLock); + } + LockUtil.lockForWrite(leaderboardsByNameLock); + try { + leaderboardsByName.clear(); + } finally { + LockUtil.unlockAfterWrite(leaderboardsByNameLock); + } + eventsById.clear(); + mediaLibrary.clear(); + competitorStore.clear(); + remoteSailingServerSet.clear(); + } + + // Used for TESTING only + @Override + public Event addEvent(String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, + String venue, boolean isPublic, UUID id) { + Event result = createEventWithoutReplication(eventName, eventDescription, startDate, endDate, venue, isPublic, + id, /* officialWebsiteURL */null, /* sailorsInfoWebsiteURLAsString */null, + /* images */Collections. emptyList(), /* videos */Collections. emptyList()); + replicate(new CreateEvent(eventName, eventDescription, startDate, endDate, venue, isPublic, id, + /* officialWebsiteURLAsString */null, /* sailorsInfoWebsiteURLAsString */null, + /* images */Collections. emptyList(), /* videos */Collections. emptyList())); + return result; + } + + @Override + public void addEventWithoutReplication(Event event) { + addEvent(event); + } + + @Override + public Event createEventWithoutReplication(String eventName, String eventDescription, TimePoint startDate, + TimePoint endDate, String venue, boolean isPublic, UUID id, URL officialWebsiteURL, URL sailorsInfoWebsiteURL, + Iterable images, Iterable videos) { + Event result = new EventImpl(eventName, startDate, endDate, venue, isPublic, id); + addEvent(result); + result.setDescription(eventDescription); + result.setOfficialWebsiteURL(officialWebsiteURL); + result.setImages(images); + result.setVideos(videos); + return result; + } + + private void addEvent(Event result) { + if (eventsById.containsKey(result.getId())) { + throw new IllegalArgumentException("Event with ID " + result.getId() + + " already exists which is pretty surprising..."); + } + eventsById.put(result.getId(), result); + mongoObjectFactory.storeEvent(result); + } + + @Override + public void updateEvent(UUID id, String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, + String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, URL sailorsInfoWebsiteURL, + Iterable images, Iterable videos) { + final Event event = eventsById.get(id); + if (event == null) { + throw new IllegalArgumentException("Sailing event with ID " + id + " does not exist."); + } + event.setName(eventName); + event.setDescription(eventDescription); + event.setStartDate(startDate); + event.setEndDate(endDate); + event.setPublic(isPublic); + event.getVenue().setName(venueName); + List leaderboardGroups = new ArrayList<>(); + for (UUID lgid : leaderboardGroupIds) { + LeaderboardGroup lg = getLeaderboardGroupByID(lgid); + if (lg != null) { + leaderboardGroups.add(lg); + } else { + logger.info("Couldn't find leaderboard group with ID " + lgid + " while updating event " + + event.getName()); + } + } + event.setLeaderboardGroups(leaderboardGroups); + event.setOfficialWebsiteURL(officialWebsiteURL); + event.setSailorsInfoWebsiteURL(sailorsInfoWebsiteURL); + event.setImages(images); + event.setVideos(videos); + // TODO consider use diffutils to compute diff between old and new leaderboard groups list and apply the patch + // to keep changes minimial + mongoObjectFactory.storeEvent(event); + } + + @Override + public void renameEvent(UUID id, String newName) { + final Event toRename = eventsById.get(id); + if (toRename == null) { + throw new IllegalArgumentException("No sailing event with ID " + id + " found."); + } + toRename.setName(newName); + mongoObjectFactory.renameEvent(id, newName); + replicate(new RenameEvent(id, newName)); + } + + @Override + public void removeEvent(UUID id) { + removeEventFromEventsById(id); + mongoObjectFactory.removeEvent(id); + replicate(new RemoveEvent(id)); + } + + protected void removeEventFromEventsById(Serializable id) { + eventsById.remove(id); + } + + @Override + public Regatta getRememberedRegattaForRace(Serializable raceID) { + return persistentRegattasForRaceIDs.get(raceID.toString()); + } + + /** + * Persistently remembers the association of the race with its {@link RaceDefinition#getId()} to the + * regatta with its {@link Regatta#getRegattaIdentifier() identifier} so that the next time + * {@link #getRememberedRegattaForRace(RaceDefinition)} is called with race as argument, + * regatta will be returned. + */ + private void setRegattaForRace(Regatta regatta, RaceDefinition race) { + setRegattaForRace(regatta, race.getId().toString()); + } + + @Override + public void setRegattaForRace(Regatta regatta, String raceIdAsString) { + persistentRegattasForRaceIDs.put(raceIdAsString, regatta); + mongoObjectFactory.storeRegattaForRaceID(raceIdAsString, regatta); + } + + @Override + public CourseArea[] addCourseAreas(UUID eventId, String[] courseAreaNames, UUID[] courseAreaIds) { + final CourseArea[] courseAreas = addCourseAreasWithoutReplication(eventId, courseAreaIds, courseAreaNames); + replicate(new AddCourseAreas(eventId, courseAreaNames, courseAreaIds)); + return courseAreas; + } + + @Override + public CourseArea[] addCourseAreasWithoutReplication(UUID eventId, UUID[] courseAreaIds, String[] courseAreaNames) { + final CourseArea[] result = new CourseArea[courseAreaNames.length]; + for (int i=0; i mediaTracks) { + mediaLibrary.addMediaTracks(mediaTracks); + } + + @Override + public void mediaTrackTitleChanged(MediaTrack mediaTrack) { + mediaDB.updateTitle(mediaTrack.dbId, mediaTrack.title); + mediaLibrary.titleChanged(mediaTrack); + replicate(new UpdateMediaTrackTitleOperation(mediaTrack)); + } + + @Override + public void mediaTrackUrlChanged(MediaTrack mediaTrack) { + mediaDB.updateUrl(mediaTrack.dbId, mediaTrack.url); + mediaLibrary.urlChanged(mediaTrack); + replicate(new UpdateMediaTrackUrlOperation(mediaTrack)); + } + + @Override + public void mediaTrackStartTimeChanged(MediaTrack mediaTrack) { + mediaDB.updateStartTime(mediaTrack.dbId, mediaTrack.startTime); + mediaLibrary.startTimeChanged(mediaTrack); + replicate(new UpdateMediaTrackStartTimeOperation(mediaTrack)); + } + + @Override + public void mediaTrackDurationChanged(MediaTrack mediaTrack) { + mediaDB.updateDuration(mediaTrack.dbId, mediaTrack.duration); + mediaLibrary.durationChanged(mediaTrack); + replicate(new UpdateMediaTrackDurationOperation(mediaTrack)); + } + + @Override + public void mediaTrackAssignedRacesChanged(MediaTrack mediaTrack) { + mediaDB.updateRace(mediaTrack.dbId, mediaTrack.assignedRaces); + mediaLibrary.assignedRacesChanged(mediaTrack); + replicate(new UpdateMediaTrackRacesOperation(mediaTrack)); + + } + + @Override + public void mediaTrackDeleted(MediaTrack mediaTrack) { + mediaDB.deleteMediaTrack(mediaTrack.dbId); + mediaLibrary.deleteMediaTrack(mediaTrack); + replicate(new RemoveMediaTrackOperation(mediaTrack)); + } + + @Override + public void mediaTracksImported(Collection mediaTracksToImport, boolean override) { + for (MediaTrack trackToImport : mediaTracksToImport) { + MediaTrack existingTrack = mediaLibrary.lookupMediaTrack(trackToImport); + if (existingTrack == null) { + mediaDB.insertMediaTrackWithId(trackToImport.dbId, trackToImport.title, trackToImport.url, + trackToImport.startTime, trackToImport.duration, trackToImport.mimeType, + trackToImport.assignedRaces); + mediaTrackAdded(trackToImport); + } else if (override) { + + // Using fine-grained update methods. + // Rationale: Changes on more than one track property are rare + // and don't justify the introduction of a new set + // of methods (including replication). + if (!Util.equalsWithNull(existingTrack.title, trackToImport.title)) { + mediaTrackTitleChanged(trackToImport); + } + if (!Util.equalsWithNull(existingTrack.url, trackToImport.url)) { + mediaTrackUrlChanged(trackToImport); + } + if (!Util.equalsWithNull(existingTrack.startTime, trackToImport.startTime)) { + mediaTrackStartTimeChanged(trackToImport); + } + if (!Util.equalsWithNull(existingTrack.duration, trackToImport.duration)) { + mediaTrackDurationChanged(trackToImport); + } + if (!Util.equalsWithNull(existingTrack.assignedRaces, trackToImport.assignedRaces)) { + mediaTrackAssignedRacesChanged(trackToImport); + } + } + } + } + + @Override + public Collection getMediaTracksForRace(RegattaAndRaceIdentifier regattaAndRaceIdentifier) { + return mediaLibrary.findMediaTracksForRace(regattaAndRaceIdentifier); + } + + @Override + public Collection getMediaTracksInTimeRange(RegattaAndRaceIdentifier regattaAndRaceIdentifier) { + TrackedRace trackedRace = getExistingTrackedRace(regattaAndRaceIdentifier); + if (trackedRace != null) { + if (trackedRace.isLive(MillisecondsTimePoint.now())) { + return mediaLibrary.findLiveMediaTracks(); + } else { + TimePoint raceStart = trackedRace.getStartOfRace() == null ? trackedRace.getStartOfTracking() + : trackedRace.getStartOfRace(); + TimePoint raceEnd = trackedRace.getEndOfRace() == null ? trackedRace.getEndOfTracking() : trackedRace + .getEndOfRace(); + return mediaLibrary.findMediaTracksInTimeRange(raceStart, raceEnd); + } + } else { + return Collections.emptyList(); + } + } + + @Override + public Collection getAllMediaTracks() { + return mediaLibrary.allTracks(); + } + + public String toString() { + return "RacingEventService: " + this.hashCode() + " Build: " + ServerInfo.getBuildVersion(); + } + + @Override + public void reloadRaceLog(String leaderboardName, String raceColumnName, String fleetName) { + Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + if (leaderboard != null) { + RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName); + if (raceColumn != null) { + Fleet fleetImpl = raceColumn.getFleetByName(fleetName); + RaceLog racelog = raceColumn.getRaceLog(fleetImpl); + if (racelog != null) { + raceColumn.reloadRaceLog(fleetImpl); + logger.info("Reloaded race log for fleet " + fleetImpl + " for race column " + raceColumn.getName() + + " for leaderboard " + leaderboard.getName()); + } + } + } + } + + @Override + public ConcurrentHashMap getPersistentRegattasForRaceIDs() { + return persistentRegattasForRaceIDs; + } + + @Override + public WindStore getWindStore() { + return windStore; + } + + @Override + public DeviceConfiguration getDeviceConfiguration(DeviceConfigurationIdentifier identifier) { + return configurationMap.getByMatch(identifier); + } + + @Override + public void createOrUpdateDeviceConfiguration(DeviceConfigurationMatcher matcher, DeviceConfiguration configuration) { + configurationMap.put(matcher, configuration); + mongoObjectFactory.storeDeviceConfiguration(matcher, configuration); + replicate(new CreateOrUpdateDeviceConfiguration(matcher, configuration)); + } + + @Override + public void removeDeviceConfiguration(DeviceConfigurationMatcher matcher) { + configurationMap.remove(matcher); + mongoObjectFactory.removeDeviceConfiguration(matcher); + replicate(new RemoveDeviceConfiguration(matcher)); + } + + @Override + public Map getAllDeviceConfigurations() { + return new HashMap(configurationMap); + } + + @Override + public TimePoint setStartTimeAndProcedure(String leaderboardName, String raceColumnName, String fleetName, + String authorName, int authorPriority, int passId, TimePoint logicalTimePoint, TimePoint startTime, + RacingProcedureType racingProcedure) { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + final TimePoint result; + if (leaderboard instanceof HasRegattaLike && raceLog != null) { + RaceState state = RaceStateImpl.create(/* race log resolver */ this, raceLog, new LogEventAuthorImpl(authorName, authorPriority)); + if (passId > raceLog.getCurrentPassId()) { + state.setAdvancePass(logicalTimePoint); + } + state.setRacingProcedure(logicalTimePoint, racingProcedure); + state.forceNewStartTime(logicalTimePoint, startTime); + result = state.getStartTime(); + } else { + result = null; + } + return result; + } + + public RaceLog getRaceLog(String leaderboardName, String raceColumnName, String fleetName) { + Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + if (leaderboard != null) { + RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName); + if (raceColumn != null) { + Fleet fleetImpl = raceColumn.getFleetByName(fleetName); + return raceColumn.getRaceLog(fleetImpl); + } + } + return null; + } + + @Override + public com.sap.sse.common.Util.Triple getStartTimeAndProcedure( + String leaderboardName, String raceColumnName, String fleetName) { + RaceLog raceLog = getRaceLog(leaderboardName, raceColumnName, fleetName); + Leaderboard leaderboard = getLeaderboardByName(leaderboardName); + final Triple result; + if (leaderboard instanceof HasRegattaLike && raceLog != null) { + ReadonlyRaceState state = ReadonlyRaceStateImpl.create(/* race log resolver */ this, raceLog); + result = new com.sap.sse.common.Util.Triple(state.getStartTime(), + raceLog.getCurrentPassId(), state.getRacingProcedure().getType()); + } else { + result = null; + } + return result; + } + + private Iterable getWindTrackerFactories() { + final Set result; + if (bundleContext == null) { + result = Collections.singleton((WindTrackerFactory) ExpeditionWindTrackerFactory.getInstance()); + } else { + ServiceTracker tracker = new ServiceTracker( + bundleContext, WindTrackerFactory.class.getName(), null); + tracker.open(); + result = new HashSet<>(); + for (WindTrackerFactory factory : tracker.getServices(new WindTrackerFactory[0])) { + result.add(factory); + } + } + return result; + } + + @Override + public GPSFixStore getGPSFixStore() { + return gpsFixStore; + } + + @Override + public RaceTracker getRaceTrackerById(Object id) { + return raceTrackersByID.get(id); + } + + @Override + public AbstractLogEventAuthor getServerAuthor() { + return raceLogEventAuthorForServer; + } + + @Override + public CompetitorStore getCompetitorStore() { + return competitorStore; + } + + @Override + public TypeBasedServiceFinderFactory getTypeBasedServiceFinderFactory() { + return serviceFinderFactory; + } + + @Override + public DataImportLockWithProgress getDataImportLock() { + return dataImportLock; + } + + @Override + public DataImportProgress createOrUpdateDataImportProgressWithReplication(UUID importOperationId, + double overallProgressPct, String subProgressName, double subProgressPct) { + // Create/Update locally + DataImportProgress progress = createOrUpdateDataImportProgressWithoutReplication(importOperationId, + overallProgressPct, subProgressName, subProgressPct); + // Create/Update on replicas + replicate(new CreateOrUpdateDataImportProgress(importOperationId, overallProgressPct, subProgressName, + subProgressPct)); + return progress; + } + + @Override + public DataImportProgress createOrUpdateDataImportProgressWithoutReplication(UUID importOperationId, + double overallProgressPct, String subProgressName, double subProgressPct) { + DataImportProgress progress = dataImportLock.getProgress(importOperationId); + boolean newObject = false; + if (progress == null) { + progress = new DataImportProgressImpl(importOperationId); + newObject = true; + } + progress.setOverAllProgressPct(overallProgressPct); + progress.setNameOfCurrentSubProgress(subProgressName); + progress.setCurrentSubProgressPct(subProgressPct); + if (newObject) { + dataImportLock.addProgress(importOperationId, progress); + } + return progress; + } + + @Override + public void setDataImportFailedWithoutReplication(UUID importOperationId, String errorMessage) { + DataImportProgress progress = dataImportLock.getProgress(importOperationId); + if (progress != null) { + progress.setFailed(); + progress.setErrorMessage(errorMessage); + } + } + + @Override + public void setDataImportFailedWithReplication(UUID importOperationId, String errorMessage) { + setDataImportFailedWithoutReplication(importOperationId, errorMessage); + replicate(new DataImportFailed(importOperationId, errorMessage)); + } + + @Override + public void setDataImportDeleteProgressFromMapTimerWithReplication(UUID importOperationId) { + setDataImportDeleteProgressFromMapTimerWithoutReplication(importOperationId); + replicate(new SetDataImportDeleteProgressFromMapTimer(importOperationId)); + } + + @Override + public void setDataImportDeleteProgressFromMapTimerWithoutReplication(UUID importOperationId) { + dataImportLock.setDeleteFromMapTimer(importOperationId); + } + + @Override + public Result search(KeywordQuery query) { + long start = System.currentTimeMillis(); + logger.info("Searching local server for " + query); + Result result = new RegattaByKeywordSearchService().search(this, query); + logger.fine("Search for " + query + " took " + (System.currentTimeMillis() - start) + "ms"); + return result; + } + + @Override + public Result searchRemotely(String remoteServerReferenceName, KeywordQuery query) { + long start = System.currentTimeMillis(); + ResultImpl result = null; + RemoteSailingServerReference remoteRef = remoteSailingServerSet + .getServerReferenceByName(remoteServerReferenceName); + if (remoteRef == null) { + result = null; + } else { + BufferedReader bufferedReader = null; + try { + try { + final URL eventsURL = new URL(remoteRef.getURL(), "sailingserver/api/v1/search?q=" + + URLEncoder.encode(query.toString(), "UTF-8")); + logger.info("Searching remote server " + remoteRef + " for " + query); + URLConnection urlConnection = eventsURL.openConnection(); + urlConnection.connect(); + bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), "UTF-8")); + JSONParser parser = new JSONParser(); + Object eventsAsObject = parser.parse(bufferedReader); + final LeaderboardGroupBaseJsonDeserializer leaderboardGroupBaseJsonDeserializer = new LeaderboardGroupBaseJsonDeserializer(); + LeaderboardSearchResultBaseJsonDeserializer deserializer = new LeaderboardSearchResultBaseJsonDeserializer( + new EventBaseJsonDeserializer(new VenueJsonDeserializer(new CourseAreaJsonDeserializer( + DomainFactory.INSTANCE)), leaderboardGroupBaseJsonDeserializer), + leaderboardGroupBaseJsonDeserializer); + result = new ResultImpl(query, + new LeaderboardSearchResultBaseRanker()); + JSONArray hitsAsJsonArray = (JSONArray) eventsAsObject; + for (Object hitAsObject : hitsAsJsonArray) { + JSONObject hitAsJson = (JSONObject) hitAsObject; + LeaderboardSearchResultBase hit = deserializer.deserialize(hitAsJson); + result.addHit(hit); + } + } finally { + if (bufferedReader != null) { + bufferedReader.close(); + } + } + } catch (IOException | ParseException e) { + logger.log(Level.INFO, + "Exception trying to fetch events from remote server " + remoteRef + ": " + e.getMessage(), e); + } + } + logger.fine("Remote search on " + remoteRef + " for " + query + " took " + (System.currentTimeMillis() - start) + + "ms"); + return result; + } + + @Override + public ReplicationMasterDescriptor getMasterDescriptor() { + return replicatingFromMaster; + } + + @Override + public void startedReplicatingFrom(ReplicationMasterDescriptor master) { + this.replicatingFromMaster = master; + } + + @Override + public void stoppedReplicatingFrom(ReplicationMasterDescriptor master) { + this.replicatingFromMaster = null; + } + + @Override + public void addOperationSentToMasterForReplication( + OperationWithResultWithIdWrapper operationWithResultWithIdWrapper) { + this.operationsSentToMasterForReplication.add(operationWithResultWithIdWrapper); + } + + @Override + public boolean hasSentOperationToMaster(OperationWithResult operation) { + return this.operationsSentToMasterForReplication.remove(operation); + } + + @Override + public FileStorageManagementService getFileStorageManagementService() { + ServiceReference ref = bundleContext + .getServiceReference(FileStorageManagementService.class); + if (ref == null) { + logger.warning("No file storage management service registered"); + return null; + } + return bundleContext.getService(ref); + } + + public void addMasterDataClassLoader(ClassLoader classLoader) { + masterDataClassLoaders.add(classLoader); + } + + public void removeMasterDataClassLoader(ClassLoader classLoader) { + masterDataClassLoaders.remove(classLoader); + } + + @Override + public ClassLoader getCombinedMasterDataClassLoader() { + JoinedClassLoader joinedClassLoader = new JoinedClassLoader(masterDataClassLoaders); + return joinedClassLoader; + } + + public void setPolarDataService(PolarDataService service) { + if (this.polarDataService == null && service != null) { + polarDataService = service; + polarDataService.registerDomainFactory(baseDomainFactory); + setPolarDataServiceOnAllTrackedRaces(service); + } + } + + private void setPolarDataServiceOnAllTrackedRaces(PolarDataService service) { + Iterable allRegattas = getAllRegattas(); + for (Regatta regatta : allRegattas) { + DynamicTrackedRegatta trackedRegatta = getTrackedRegatta(regatta); + if (trackedRegatta != null) { + trackedRegatta.lockTrackedRacesForRead(); + try { + Iterable trackedRaces = trackedRegatta.getTrackedRaces(); + for (TrackedRace trackedRace : trackedRaces) { + trackedRace.setPolarDataService(service); + service.insertExistingFixes(trackedRace); + } + } finally { + trackedRegatta.unlockTrackedRacesAfterRead(); + } + } + } + } + + public void unsetPolarDataService(PolarDataService service) { + if (polarDataService == service) { + polarDataService.unregisterDomainFactory(baseDomainFactory); + polarDataService = null; + setPolarDataService(null); + } + } + + @Override + public Iterable getCompetitorInOrderOfWindwardDistanceTraveledFarthestFirst(TrackedRace trackedRace, TimePoint timePoint) { + final RankingInfo rankingInfo = trackedRace.getRankingMetric().getRankingInfo(timePoint); + final List result = new ArrayList<>(); + final Map windwardDistanceSailedPerCompetitor = new HashMap<>(); + for (final Competitor competitor : trackedRace.getRace().getCompetitors()) { + result.add(competitor); + final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); + windwardDistanceSailedPerCompetitor.put(competitor, competitorRankingInfo == null ? null : competitorRankingInfo.getWindwardDistanceSailed()); + } + final Comparator durationComparatorNullsLast = Comparator.nullsLast(Comparator.naturalOrder()); + result.sort((c1, c2) -> durationComparatorNullsLast.compare(windwardDistanceSailedPerCompetitor.get(c2), + windwardDistanceSailedPerCompetitor.get(c1))); + return result; + } + + /** + * A {@link SimpleRaceLogIdentifier} in particular has a {@link SimpleRaceLogIdentifier#getRegattaLikeParentName()} + * which identifies either a regatta by name or a flexible leaderboard by name. Here is why this can luckily be + * resolved unanimously: A regatta leaderboard always uses as its name the regatta name (see + * {@link RegattaImpl#getName()}). Trying to {@link RegattaLeaderboardImpl#setName(String) set} the regatta leaderboard's + * name can only update its {@link Leaderboard#getDisplayName() display name}. Therefore, regatta leaderboards are always + * keyed in {@link #leaderboardsByName} by their regatta's name. Thus, no flexible leaderboard can have a regatta's name + * as its name, and therefore leaderboard names and regatta names are unitedly unique. + */ + @Override + public RaceLog resolve(SimpleRaceLogIdentifier identifier) { + final RaceLog result; + final IsRegattaLike regattaLike; + final Regatta regatta = regattasByName.get(identifier.getRegattaLikeParentName()); + if (regatta != null) { + regattaLike = regatta; + } else { + final Leaderboard leaderboard = leaderboardsByName.get(identifier.getRegattaLikeParentName()); + if (leaderboard != null && leaderboard instanceof FlexibleLeaderboard) { + regattaLike = (FlexibleLeaderboard) leaderboard; + } else { + regattaLike = null; + } + } + if (regattaLike != null) { + final RaceColumn raceColumn = regattaLike.getRaceColumnByName(identifier.getRaceColumnName()); + if (raceColumn != null) { + final Fleet fleet = raceColumn.getFleetByName(identifier.getFleetName()); + if (fleet != null) { + result = raceColumn.getRaceLog(fleet); + } else { + result = null; + } + } else { + result = null; + } + } else { + result = null; + } + return result; + } +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateEvent.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateEvent.java index 0eaa2c1dd2b..1dcb15b294d 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateEvent.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateEvent.java @@ -1,70 +1,70 @@ -package com.sap.sailing.server.operationaltransformation; - -import java.net.URL; -import java.util.UUID; - -import com.sap.sailing.domain.base.Event; -import com.sap.sailing.server.RacingEventService; -import com.sap.sailing.server.RacingEventServiceOperation; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.media.ImageDescriptor; -import com.sap.sse.common.media.VideoDescriptor; - -/** - * Creates an {@link Event} in the server, with a new venue and an empty course area list. - * See the {@link AddCourseAreas} operation for adding course areas to the event's venue. - * - * @author Axel Uhl (d043530) - * - */ -public class CreateEvent extends AbstractEventOperation { - private static final long serialVersionUID = 308389324918359960L; - private final String venue; - private final TimePoint startDate; - private final TimePoint endDate; - private final boolean isPublic; - private final String eventName; - private final String eventDescription; - private final Iterable images; - private final Iterable videos; - private final URL officialWebsiteURL; - private final URL sailorsInfoWebsiteURL; - - public CreateEvent(String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, String venue, - boolean isPublic, UUID id, URL officialWebsiteURL, URL sailorsInfoWebsiteURL, Iterable images, Iterable videos) { - super(id); - this.eventName = eventName; - this.eventDescription = eventDescription; - this.startDate = startDate; - this.endDate = endDate; - this.venue = venue; - this.isPublic = isPublic; - this.officialWebsiteURL = officialWebsiteURL; - this.sailorsInfoWebsiteURL = sailorsInfoWebsiteURL; - this.images = images; - this.videos = videos; - } - - @Override - public RacingEventServiceOperation transformClientOp(RacingEventServiceOperation serverOp) { - // TODO Auto-generated method stub - return null; - } - - @Override - public RacingEventServiceOperation transformServerOp(RacingEventServiceOperation clientOp) { - // TODO Auto-generated method stub - return null; - } - - protected String getEventName() { - return eventName; - } - - @Override - public Event internalApplyTo(RacingEventService toState) { - return toState.createEventWithoutReplication(getEventName(), eventDescription, startDate, endDate, venue, isPublic, - getId(), officialWebsiteURL, sailorsInfoWebsiteURL, images, videos); - } - -} +package com.sap.sailing.server.operationaltransformation; + +import java.net.URL; +import java.util.UUID; + +import com.sap.sailing.domain.base.Event; +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.RacingEventServiceOperation; +import com.sap.sse.common.TimePoint; +import com.sap.sse.shared.media.ImageDescriptor; +import com.sap.sse.shared.media.VideoDescriptor; + +/** + * Creates an {@link Event} in the server, with a new venue and an empty course area list. + * See the {@link AddCourseAreas} operation for adding course areas to the event's venue. + * + * @author Axel Uhl (d043530) + * + */ +public class CreateEvent extends AbstractEventOperation { + private static final long serialVersionUID = 308389324918359960L; + private final String venue; + private final TimePoint startDate; + private final TimePoint endDate; + private final boolean isPublic; + private final String eventName; + private final String eventDescription; + private final Iterable images; + private final Iterable videos; + private final URL officialWebsiteURL; + private final URL sailorsInfoWebsiteURL; + + public CreateEvent(String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, String venue, + boolean isPublic, UUID id, URL officialWebsiteURL, URL sailorsInfoWebsiteURL, Iterable images, Iterable videos) { + super(id); + this.eventName = eventName; + this.eventDescription = eventDescription; + this.startDate = startDate; + this.endDate = endDate; + this.venue = venue; + this.isPublic = isPublic; + this.officialWebsiteURL = officialWebsiteURL; + this.sailorsInfoWebsiteURL = sailorsInfoWebsiteURL; + this.images = images; + this.videos = videos; + } + + @Override + public RacingEventServiceOperation transformClientOp(RacingEventServiceOperation serverOp) { + // TODO Auto-generated method stub + return null; + } + + @Override + public RacingEventServiceOperation transformServerOp(RacingEventServiceOperation clientOp) { + // TODO Auto-generated method stub + return null; + } + + protected String getEventName() { + return eventName; + } + + @Override + public Event internalApplyTo(RacingEventService toState) { + return toState.createEventWithoutReplication(getEventName(), eventDescription, startDate, endDate, venue, isPublic, + getId(), officialWebsiteURL, sailorsInfoWebsiteURL, images, videos); + } + +} diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java index bc3d64901a6..eb9c60ca2a0 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/UpdateEvent.java @@ -1,61 +1,61 @@ -package com.sap.sailing.server.operationaltransformation; - -import java.net.URL; -import java.util.UUID; - -import com.sap.sailing.server.RacingEventService; -import com.sap.sailing.server.RacingEventServiceOperation; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.media.ImageDescriptor; -import com.sap.sse.common.media.VideoDescriptor; - -public class UpdateEvent extends AbstractEventOperation { - private static final long serialVersionUID = -8271559266421161532L; - private final String venueName; - private final TimePoint startDate; - private final TimePoint endDate; - private final boolean isPublic; - private final Iterable leaderboardGroupIds; - private final String eventName; - private final String eventDescription; - private final URL officialWebsiteURL; - private final URL sailorsInfoWebsiteURL; - private final Iterable images; - private final Iterable videos; - - public UpdateEvent(UUID id, String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, - String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, URL sailorsInfoWebsiteURL, - Iterable images, Iterable videos) { - super(id); - this.eventName = eventName; - this.eventDescription = eventDescription; - this.startDate = startDate; - this.endDate = endDate; - this.venueName = venueName; - this.isPublic = isPublic; - this.leaderboardGroupIds = leaderboardGroupIds; - this.officialWebsiteURL = officialWebsiteURL; - this.sailorsInfoWebsiteURL = sailorsInfoWebsiteURL; - this.images = images; - this.videos = videos; - } - - @Override - public RacingEventServiceOperation transformClientOp(RacingEventServiceOperation serverOp) { - // TODO Auto-generated method stub - return null; - } - - @Override - public RacingEventServiceOperation transformServerOp(RacingEventServiceOperation clientOp) { - // TODO Auto-generated method stub - return null; - } - - @Override - public Void internalApplyTo(RacingEventService toState) { - toState.updateEvent(getId(), eventName, eventDescription, startDate, endDate, venueName, isPublic, - leaderboardGroupIds, officialWebsiteURL, sailorsInfoWebsiteURL, images, videos); - return null; - } -} +package com.sap.sailing.server.operationaltransformation; + +import java.net.URL; +import java.util.UUID; + +import com.sap.sailing.server.RacingEventService; +import com.sap.sailing.server.RacingEventServiceOperation; +import com.sap.sse.common.TimePoint; +import com.sap.sse.shared.media.ImageDescriptor; +import com.sap.sse.shared.media.VideoDescriptor; + +public class UpdateEvent extends AbstractEventOperation { + private static final long serialVersionUID = -8271559266421161532L; + private final String venueName; + private final TimePoint startDate; + private final TimePoint endDate; + private final boolean isPublic; + private final Iterable leaderboardGroupIds; + private final String eventName; + private final String eventDescription; + private final URL officialWebsiteURL; + private final URL sailorsInfoWebsiteURL; + private final Iterable images; + private final Iterable videos; + + public UpdateEvent(UUID id, String eventName, String eventDescription, TimePoint startDate, TimePoint endDate, + String venueName, boolean isPublic, Iterable leaderboardGroupIds, URL officialWebsiteURL, URL sailorsInfoWebsiteURL, + Iterable images, Iterable videos) { + super(id); + this.eventName = eventName; + this.eventDescription = eventDescription; + this.startDate = startDate; + this.endDate = endDate; + this.venueName = venueName; + this.isPublic = isPublic; + this.leaderboardGroupIds = leaderboardGroupIds; + this.officialWebsiteURL = officialWebsiteURL; + this.sailorsInfoWebsiteURL = sailorsInfoWebsiteURL; + this.images = images; + this.videos = videos; + } + + @Override + public RacingEventServiceOperation transformClientOp(RacingEventServiceOperation serverOp) { + // TODO Auto-generated method stub + return null; + } + + @Override + public RacingEventServiceOperation transformServerOp(RacingEventServiceOperation clientOp) { + // TODO Auto-generated method stub + return null; + } + + @Override + public Void internalApplyTo(RacingEventService toState) { + toState.updateEvent(getId(), eventName, eventDescription, startDate, endDate, venueName, isPublic, + leaderboardGroupIds, officialWebsiteURL, sailorsInfoWebsiteURL, images, videos); + return null; + } +} diff --git a/java/com.sap.sse.common/pom.xml b/java/com.sap.sse.common/pom.xml index de52eed27a2..e1006e659cc 100644 --- a/java/com.sap.sse.common/pom.xml +++ b/java/com.sap.sse.common/pom.xml @@ -11,30 +11,30 @@ eclipse-plugin - - - org.eclipse.tycho - tycho-compiler-plugin - ${tycho-version} - - 1.7 - 1.7 - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + 1.7 + 1.7 + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + diff --git a/java/com.sap.sse.datamining.annotations/pom.xml b/java/com.sap.sse.datamining.annotations/pom.xml index fa8f86b4af2..343b2434435 100644 --- a/java/com.sap.sse.datamining.annotations/pom.xml +++ b/java/com.sap.sse.datamining.annotations/pom.xml @@ -1,31 +1,31 @@ - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - com.sap.sse.datamining.annotations - eclipse-plugin - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" + xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sse.datamining.annotations + eclipse-plugin + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/IntegerBox.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/IntegerBox.java index 27273086f30..f5a5b18b7cb 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/IntegerBox.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/IntegerBox.java @@ -7,6 +7,9 @@ import com.google.gwt.text.client.IntegerParser; import com.google.gwt.text.shared.Renderer; import com.google.gwt.user.client.ui.ValueBox; +/** + * Renders integers in their box without any separators, non-localized, plain. + */ public class IntegerBox extends ValueBox { private static final Renderer RENDERER = new Renderer() { public String render(Integer object) { diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java index 93ffbc08988..3f31188524e 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/SliderBar.java @@ -1,1256 +1,1256 @@ -package com.sap.sse.gwt.client.controls.slider; - -/* - * Copyright 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -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.Element; -import com.google.gwt.dom.client.Style.Display; -import com.google.gwt.dom.client.Style.Position; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.dom.client.Style.Visibility; -import com.google.gwt.event.dom.client.KeyCodes; -import com.google.gwt.event.logical.shared.HasValueChangeHandlers; -import com.google.gwt.event.logical.shared.ValueChangeEvent; -import com.google.gwt.event.logical.shared.ValueChangeHandler; -import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.resources.client.ClientBundle; -import com.google.gwt.resources.client.CssResource; -import com.google.gwt.resources.client.CssResource.NotStrict; -import com.google.gwt.resources.client.ImageResource; -import com.google.gwt.user.client.DOM; -import com.google.gwt.user.client.Event; -import com.google.gwt.user.client.Timer; -import com.google.gwt.user.client.Window; -import com.google.gwt.user.client.ui.FocusPanel; -import com.google.gwt.user.client.ui.HasValue; -import com.google.gwt.user.client.ui.Image; -import com.google.gwt.user.client.ui.RequiresResize; -import com.sap.sse.common.Util; - -/** - * A widget that allows the user to select a value within a range of possible values using a sliding bar that responds - * to mouse events. - * - *

    Keyboard Events

    - *

    - * SliderBar listens for the following key events. Holding down a key will repeat the action until the key is released. - *

      - *
    • left arrow - shift left one step
    • - *
    • right arrow - shift right one step
    • - *
    • ctrl+left arrow - jump left 10% of the distance
    • - *
    • ctrl+right arrow - jump right 10% of the distance
    • - *
    • home - jump to min value
    • - *
    • end - jump to max value
    • - *
    • space - jump to middle value
    • - *
    - *

    - * - *

    CSS Style Rules

    - *
      - *
    • .gwt-SliderBar-shell { primary style }
    • - *
    • .gwt-SliderBar-shell-focused { primary style when focused }
    • - *
    • .gwt-SliderBar-shell gwt-SliderBar-line { the line that the knob moves along }
    • - *
    • .gwt-SliderBar-shell gwt-SliderBar-line-sliding { the line that the knob moves along when sliding }
    • - *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob { the sliding knob }
    • - *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob-sliding { the sliding knob when sliding }
    • - *
    • - * .gwt-SliderBar-shell .gwt-SliderBar-tick { the ticks along the line }
    • - *
    • .gwt-SliderBar-shell .gwt-SliderBar-ticklabel { the text labels along the line }
    • - *
    - */ -public class SliderBar extends FocusPanel implements RequiresResize, HasValue, HasValueChangeHandlers { - /** - * A formatter used to format the labels displayed in the widget. - */ - public static interface LabelFormatter { - /** - * Generate the text to display in each label based on the label's value. - * - * Override this method to change the text displayed within the SliderBar. - * - * @param slider - * the Slider bar - * @param value - * the value the label displays - * @return the text to display for the label - */ - String formatLabel(SliderBar slider, Double value, Double previousValue); - } - - /** - * A {@link ClientBundle} that provides images for {@link SliderBar}. - */ - public static interface SliderBarImages extends ClientBundle { - public static final SliderBarImages INSTANCE = GWT.create(SliderBarImages.class); - - /** - * An image used for the sliding knob. - * - * @return a prototype of this image - */ - @Source("slider_blue.png") - ImageResource slider(); - - /** - * An image used for the disabled sliding knob. - * - * @return a prototype of this image - */ - @Source("slider_blue.png") - ImageResource sliderDisabled(); - - /** - * An image used for the sliding knob while sliding. - * - * @return a prototype of this image - */ - @Source("slider_blue.png") - ImageResource sliderSliding(); - - @NotStrict - @Source("SliderBar.css") - CssResource sliderBarCss(); - } - - /** - * The timer used to continue to shift the knob as the user holds down one of the left/right arrow keys. Only IE - * auto-repeats, so we just keep catching the events. - */ - private class KeyTimer extends Timer { - /** - * A bit indicating that this is the first run. - */ - private boolean firstRun = true; - - /** - * The delay between shifts, which shortens as the user holds down the button. - */ - private int repeatDelay = 30; - - /** - * A bit indicating whether we are shifting to a higher or lower value. - */ - private boolean shiftRight = false; - - /** - * The number of steps to shift with each press. - */ - private int multiplier = 1; - - /** - * This method will be called when a timer fires. Override it to implement the timer's logic. - */ - @Override - public void run() { - // Highlight the knob on first run - if (firstRun) { - firstRun = false; - startSliding(true, false); - } - - // Slide the slider bar - if (shiftRight) { - setCurrentValue(curValue + multiplier * stepSize); - } else { - setCurrentValue(curValue - multiplier * stepSize); - } - - // Repeat this timer until cancelled by keyup event - schedule(repeatDelay); - } - - /** - * Schedules a timer to elapse in the future. - * - * @param delayMillis - * how long to wait before the timer elapses, in milliseconds - * @param shiftRight - * whether to shift up or not - * @param multiplier - * the number of steps to shift - */ - public void schedule(int delayMillis, boolean shiftRight, int multiplier) { - firstRun = true; - this.shiftRight = shiftRight; - this.multiplier = multiplier; - super.schedule(delayMillis); - } - } - - /** - * The current value. - */ - protected Double curValue; - - /** - * The knob that slides across the line. - */ - protected Image knobImage = new Image(); - - /** - * The timer used to continue to shift the knob if the user holds down a key. - */ - private KeyTimer keyTimer = new KeyTimer(); - - /** - * The elements used to display labels above the ticks. - */ - protected List tickLabelElements = new ArrayList(); - - /** - * The elements used to display the marker labels. - */ - protected List markerLabelElements = new ArrayList(); - - /** - * The formatter used to generate label text. - */ - protected LabelFormatter tickLabelFormatter; - - /** - * The line that the knob moves over. - */ - protected Element lineElement; - - /** - * The offset between the edge of the shell and the line. - */ - protected int lineLeftOffset = 0; - - /** - * The maximum slider value. - */ - protected Double maxValue; - - /** - * The minimum slider value. - */ - protected Double minValue; - - /** - * The number of labels to show. - */ - private int numTickLabels = 0; - - /** - * The number of tick marks to show. - */ - protected int numTicks = 0; - - /** - * A bit indicating whether or not we are currently sliding the slider bar due to keyboard events. - */ - private boolean slidingKeyboard = false; - - /** - * A bit indicating whether or not we are currently sliding the slider bar due to mouse events. - */ - private boolean slidingMouse = false; - - /** - * A bit indicating whether or not the slider is enabled - */ - protected boolean enabled = true; - - /** - * The images used with the sliding bar. - */ - private SliderBarImages images; - - /** - * The size of the increments between knob positions. - */ - protected double stepSize; - - /** - * The elements used to display tick marks, which are the vertical lines along the slider bar. - */ - protected List tickElements = new ArrayList(); - - /** - * The elements used to display additional markers on the slider bar. - */ - protected List markerElements = new ArrayList(); - - private List markers = new ArrayList(); - - private class Marker { - String name; - Double position; - - public Marker(String name, Double position) { - super(); - this.name = name; - this.position = position; - } - } - - /** - * Create a slider bar. - */ - public SliderBar() { - this(null, null, null); - } - - /** - * Create a slider bar. - * - * @param minValue - * the minimum value in the range - * @param maxValue - * the maximum value in the range - */ - public SliderBar(double minValue, double maxValue) { - this(minValue, maxValue, null); - } - - /** - * Create a slider bar. - * - * @param minValue - * the minimum value in the range - * @param maxValue - * the maximum value in the range - * @param tickLabelFormatter - * the label formatter - */ - public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter) { - this(minValue, maxValue, tickLabelFormatter, SliderBarImages.INSTANCE); - } - - /** - * Create a slider bar. - * - * @param minValue - * the minimum value in the range - * @param maxValue - * the maximum value in the range - * @param tickLabelFormatter - * the label formatter - * @param images - * the images to use for the slider - */ - public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter, SliderBarImages images) { - super(); - images.sliderBarCss().ensureInjected(); - this.minValue = minValue; - this.maxValue = maxValue; - this.images = images; - setLabelFormatter(tickLabelFormatter); - - // Create the outer shell - getElement().getStyle().setPosition(Position.RELATIVE); - setStyleName("gwt-SliderBar-shell"); - - // Create the line - lineElement = DOM.createDiv(); - DOM.appendChild(getElement(), lineElement); - lineElement.getStyle().setPosition(Position.ABSOLUTE); - lineElement.setPropertyString("className", "gwt-SliderBar-line"); - - // Create the knob - knobImage.setResource(images.slider()); - Element knobElement = knobImage.getElement(); - DOM.appendChild(getElement(), knobElement); - knobElement.getStyle().setPosition(Position.ABSOLUTE); - knobElement.setPropertyString("className", "gwt-SliderBar-knob"); - - sinkEvents(Event.MOUSEEVENTS | Event.KEYEVENTS | Event.FOCUSEVENTS); - - // workaround to render properly when parent Widget does not - // implement ProvidesResize since DOM doesn't provide element - // height and width until onModuleLoad() finishes. - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - @Override - public void execute() { - onResize(); - } - }); - } - - public boolean isMinMaxInitialized() { - return minValue != null && maxValue != null; - } - - public HandlerRegistration addValueChangeHandler(ValueChangeHandler handler) { - return addHandler(handler, ValueChangeEvent.getType()); - } - - /** - * Return the current value. - * - * @return the current value - */ - public Double getCurrentValue() { - return curValue; - } - - /** - * Return the label formatter. - * - * @return the label formatter - */ - public LabelFormatter getLabelFormatter() { - return tickLabelFormatter; - } - - /** - * Return the max value. - * - * @return the max value - */ - public Double getMaxValue() { - return maxValue; - } - - /** - * Return the minimum value. - * - * @return the minimum value - */ - public Double getMinValue() { - return minValue; - } - - /** - * Return the number of labels. - * - * @return the number of labels - */ - public int getNumLabels() { - return numTickLabels; - } - - /** - * Return the number of ticks. - * - * @return the number of ticks - */ - public int getNumTicks() { - return numTicks; - } - - /** - * Return the step size. - * - * @return the step size - */ - public double getStepSize() { - return stepSize; - } - - /** - * Return the total range between the minimum and maximum values. - * - * @return the total range - */ - public double getTotalRange() { - if (minValue == null || maxValue == null || minValue > maxValue) { - return 0; - } else { - return maxValue - minValue; - } - } - - public Double getValue() { - return curValue; - } - - /** - * @return Gets whether this widget is enabled - */ - public boolean isEnabled() { - return enabled; - } - - /** - * Listen for events that will move the knob. - * - * @param event - * the event that occurred - */ - @Override - public void onBrowserEvent(Event event) { - super.onBrowserEvent(event); - if (enabled) { - switch (DOM.eventGetType(event)) { - // Unhighlight and cancel keyboard events - case Event.ONBLUR: - keyTimer.cancel(); - if (slidingMouse) { - DOM.releaseCapture(getElement()); - slidingMouse = false; - slideKnob(event); - stopSliding(true, true); - } else if (slidingKeyboard) { - slidingKeyboard = false; - stopSliding(true, true); - } - unhighlight(); - break; - - // Highlight on focus - case Event.ONFOCUS: - highlight(); - break; - - // Mousewheel events - case Event.ONMOUSEWHEEL: - int velocityY = event.getMouseWheelVelocityY(); - event.preventDefault(); - if (velocityY > 0) { - shiftRight(1); - } else { - shiftLeft(1); - } - break; - - // Shift left or right on key press - case Event.ONKEYDOWN: - if (!slidingKeyboard) { - int multiplier = 1; - if (event.getCtrlKey()) { - multiplier = (int) (getTotalRange() / stepSize / 10); - } - - switch (event.getKeyCode()) { - case KeyCodes.KEY_HOME: - event.preventDefault(); - setCurrentValue(minValue); - break; - case KeyCodes.KEY_END: - event.preventDefault(); - setCurrentValue(maxValue); - break; - case KeyCodes.KEY_LEFT: - event.preventDefault(); - slidingKeyboard = true; - startSliding(false, true); - shiftLeft(multiplier); - keyTimer.schedule(400, false, multiplier); - break; - case KeyCodes.KEY_RIGHT: - event.preventDefault(); - slidingKeyboard = true; - startSliding(false, true); - shiftRight(multiplier); - keyTimer.schedule(400, true, multiplier); - break; - case 32: - event.preventDefault(); - setCurrentValue(minValue + getTotalRange() / 2); - break; - } - } - break; - // Stop shifting on key up - case Event.ONKEYUP: - keyTimer.cancel(); - if (slidingKeyboard) { - slidingKeyboard = false; - stopSliding(true, true); - } - break; - - // Mouse Events - case Event.ONMOUSEDOWN: - setFocus(true); - slidingMouse = true; - DOM.setCapture(getElement()); - startSliding(true, true); - event.preventDefault(); - slideKnob(event); - break; - case Event.ONMOUSEUP: - if (slidingMouse) { - DOM.releaseCapture(getElement()); - slidingMouse = false; - slideKnob(event); - stopSliding(true, true); - } - break; - case Event.ONMOUSEMOVE: - if (slidingMouse) { - slideKnob(event); - } - break; - } - } - } - - /** - * This method is called when the dimensions of the parent element change. Subclasses should override this method as - * needed. - * - * @param width - * the new client width of the element - * @param height - * the new client height of the element - */ - public void onResize(int width, int height) { - // Center the line in the shell - int lineWidth = lineElement.getOffsetWidth(); - lineLeftOffset = (width / 2) - (lineWidth / 2); - lineElement.getStyle().setLeft(lineLeftOffset, Unit.PX); - - // Draw the other components - drawTickLabels(); - drawTicks(); - drawMarkers(); - drawMarkerLabels(); - drawKnob(); - } - - /** - * Redraw the progress bar when something changes the layout. - */ - public void redraw() { - if (isAttached()) { - int width = getElement().getClientWidth(); - int height = getElement().getClientHeight(); - onResize(width, height); - } - } - - /** - * Set the current value and fire the onValueChange event. - * - * @param curValue - * the current value - */ - public synchronized void setCurrentValue(Double curValue) { - setCurrentValue(curValue, true); - } - - /** - * Set the current value and optionally fire the onValueChange event. - * - * @param curValue - * the current value - * @param fireEvent - * fire the onValue change event if true - */ - public synchronized void setCurrentValue(Double curValue, boolean fireEvent) { - // Confine the value to the range - if (!isMinMaxInitialized() || curValue == null) { - return; - } - this.curValue = Math.max(minValue, Math.min(maxValue, curValue)); - double remainder = (this.curValue - minValue) % stepSize; - this.curValue -= remainder; - // Go to next step if more than halfway there - if ((remainder > (stepSize / 2)) && ((this.curValue + stepSize) <= maxValue)) { - this.curValue += stepSize; - } - // Redraw the knob - drawKnob(); - // Fire the ValueChangeEvent if the value actually changed - if (fireEvent && !curValue.equals(this.curValue)) { - ValueChangeEvent.fire(this, this.curValue); - } - } - - /** - * Sets whether this widget is enabled. - * - * @param enabled - * true to enable the widget, false to disable it - */ - public void setEnabled(boolean enabled) { - this.enabled = enabled; - if (enabled) { - knobImage.setResource(images.slider()); - lineElement.setPropertyString("className", "gwt-SliderBar-line"); - } else { - knobImage.setResource(images.sliderDisabled()); - lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-disabled"); - } - redraw(); - } - - /** - * Set the label formatter. - * - * @param labelFormatter - * the label formatter - */ - public void setLabelFormatter(LabelFormatter labelFormatter) { - this.tickLabelFormatter = labelFormatter; - } - - /** - * Set the max value. - * - * @param maxValue - * the current value - */ - public void setMaxValue(Double maxValue, boolean fireEvent) { - if (!Util.equalsWithNull(maxValue, this.maxValue)) { - this.maxValue = maxValue; - onMinMaxValueChanged(fireEvent); - } - } - - /** - * Set the minimum value. - * - * @param minValue - * the current value - */ - public void setMinValue(Double minValue, boolean fireEvent) { - if (!Util.equalsWithNull(minValue, this.minValue)) { - this.minValue = minValue; - onMinMaxValueChanged(fireEvent); - } - } - - /** - * Set the minimum and maximum value - * - * @param minValue - * the current value for min - * @param maxValue - * the current valuefor max - */ - public void setMinAndMaxValue(Double minValue, Double maxValue, boolean fireEvent) { - boolean changed = false; - if (!Util.equalsWithNull(minValue, this.minValue)) { - this.minValue = minValue; - changed = true; - } - if (!Util.equalsWithNull(maxValue, this.maxValue)) { - this.maxValue = maxValue; - changed = true; - } - if (changed) { - onMinMaxValueChanged(fireEvent); - } - } - - /** - * Handle value changes of min and/or max value - */ - protected void onMinMaxValueChanged(boolean fireEvent) { - redraw(); - resetCurrentValue(fireEvent); - } - - /** - * Set the number of tick labels to show on the line. Tick labels indicate the value of the slider at that point. - * Use this method to enable tick labels. - * - * If you set the number of tick labels equal to the total range divided by the step size, you will get a properly - * aligned "jumping" effect where the knob jumps between tick labels. - * - * Note that the number of tick labels displayed will be one more than the number you specify, so specify 1 labels - * to show labels on either end of the line. In other words, numTickLabels is really the number of slots between the - * labels. - * - * setNumTickLabels(0) will disable the labels. - * - * @param numTickLabels - * the number of tick labels to show - */ - public void setNumTickLabels(int numTickLabels) { - this.numTickLabels = numTickLabels; - drawTickLabels(); - } - - /** - * Set the number of ticks to show on the line. A tick is a vertical line that represents a division of the overall - * line. Use this method to enable ticks. - * - * If you set the number of ticks equal to the total range divided by the step size, you will get a properly aligned - * "jumping" effect where the knob jumps between ticks. - * - * Note that the number of ticks displayed will be one more than the number you specify, so specify 1 tick to show - * ticks on either end of the line. In other words, numTicks is really the number of slots between the ticks. - * - * setNumTicks(0) will disable ticks. - * - * @param numTicks - * the number of ticks to show - */ - public void setNumTicks(int numTicks) { - this.numTicks = numTicks; - drawTicks(); - } - - /** - * Set the step size. - * - * @param stepSize - * the current value - */ - public void setStepSize(double stepSize, boolean fireEvent) { - this.stepSize = stepSize; - resetCurrentValue(fireEvent); - } - - public void setValue(Double value) { - setCurrentValue(value, false); - } - - public void setValue(Double value, boolean fireEvent) { - setCurrentValue(value, fireEvent); - } - - /** - * Shift to the left (smaller value). - * - * @param numSteps - * the number of steps to shift - */ - public void shiftLeft(int numSteps) { - setCurrentValue(getCurrentValue() - numSteps * stepSize); - } - - /** - * Shift to the right (greater value). - * - * @param numSteps - * the number of steps to shift - */ - public void shiftRight(int numSteps) { - setCurrentValue(getCurrentValue() + numSteps * stepSize); - } - - /** - * Format the label to display above the ticks - * - * Override this method in a subclass to customize the format. By default, this method returns the integer portion - * of the value. - * - * @param value - * the value at the label - * @return the text to put in the label - */ - protected String formatTickLabel(Double value, Double previousValue) { - if (tickLabelFormatter != null) { - return tickLabelFormatter.formatLabel(this, value, previousValue); - } else { - return (int) (10 * value) / 10.0 + ""; - } - } - - /** - * Get the percentage of the knob's position relative to the size of the line. The return value will be between 0.0 - * and 1.0. - * - * @return the current percent complete - */ - protected double getKnobPercent() { - // If we have no range - if (maxValue <= minValue) { - return 0; - } - - // Calculate the relative progress - double percent = (curValue - minValue) / (maxValue - minValue); - return Math.max(0.0, Math.min(1.0, percent)); - } - - /** - * This method is called immediately after a widget becomes attached to the browser's document. - */ - @Override - protected void onLoad() { - // Reset the position attribute of the parent element - getElement().getStyle().setPosition(Position.RELATIVE); - } - - /** - * Draw the knob where it is supposed to be relative to the line. - */ - protected void drawKnob() { - if (isAttached() && isMinMaxInitialized()) { - // Move the knob to the correct position - Element knobElement = knobImage.getElement(); - int lineWidth = lineElement.getOffsetWidth(); - int knobWidth = knobElement.getOffsetWidth(); - int knobLeftOffset = (int) (lineLeftOffset + (getKnobPercent() * lineWidth) - (knobWidth / 2)); - knobLeftOffset = Math.min(knobLeftOffset, lineLeftOffset + lineWidth - (knobWidth / 2) - 1); - knobElement.getStyle().setLeft(knobLeftOffset, Unit.PX); - } - } - - /** - * Draw the labels along the line. - */ - protected void drawTickLabels() { - if (isAttached() && isMinMaxInitialized()) { - // Draw the tick labels - int lineWidth = lineElement.getOffsetWidth(); - if (numTickLabels > 0) { - // Create the labels or make them visible - Double previousValue = null; - for (int i = 0; i <= numTickLabels; i++) { - Element label = null; - if (i < tickLabelElements.size()) { - label = tickLabelElements.get(i); - } else { // Create the new label - label = DOM.createDiv(); - label.getStyle().setPosition(Position.ABSOLUTE); - label.getStyle().setDisplay(Display.NONE); - if (enabled) { - label.setPropertyString("className", "gwt-SliderBar-ticklabel"); - } else { - label.setPropertyString("className", "gwt-SliderBar-ticklabel-disabled"); - } - DOM.appendChild(getElement(), label); - tickLabelElements.add(label); - } - - // Set the label text - double value = minValue + (getTotalRange() * i / numTickLabels); - label.getStyle().setVisibility(Visibility.HIDDEN); - label.getStyle().setProperty("display", ""); - label.setPropertyString("innerHTML", formatTickLabel(value, previousValue)); - - // Move to the left so the label width is not clipped by the shell - label.getStyle().setLeft(0, Unit.PX); - - // Position the label and make it visible - int labelWidth = label.getOffsetWidth(); - int labelLeftOffset = lineLeftOffset + (lineWidth * i / numTickLabels) - (labelWidth / 2); - labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); - labelLeftOffset = Math.max(labelLeftOffset, lineLeftOffset); - label.getStyle().setLeft(labelLeftOffset, Unit.PX); - label.getStyle().setVisibility(Visibility.VISIBLE); - - previousValue = value; - } - - // Hide unused labels - for (int i = (numTickLabels + 1); i < tickLabelElements.size(); i++) { - tickLabelElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all labels - for (Element elem : tickLabelElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Draw the tick along the line. - */ - protected void drawTicks() { - if (isAttached() && isMinMaxInitialized()) { - // Draw the ticks - int lineWidth = lineElement.getOffsetWidth(); - if (numTicks > 0) { - // Create the ticks or make them visible - for (int i = 0; i <= numTicks; i++) { - Element tick = null; - if (i < tickElements.size()) { - tick = tickElements.get(i); - } else { // Create the new tick - tick = DOM.createDiv(); - tick.getStyle().setPosition(Position.ABSOLUTE); - tick.getStyle().setDisplay(Display.NONE); - DOM.appendChild(getElement(), tick); - tickElements.add(tick); - } - if (enabled) { - tick.setPropertyString("className", "gwt-SliderBar-tick"); - } else { - tick.setPropertyString("className", "gwt-SliderBar-tick gwt-SliderBar-tick-disabled"); - } - // Position the tick and make it visible - tick.getStyle().setVisibility(Visibility.HIDDEN); - tick.getStyle().setProperty("display", ""); - int tickWidth = tick.getOffsetWidth(); - int tickLeftOffset = lineLeftOffset + (lineWidth * i / numTicks) - (tickWidth / 2); - tickLeftOffset = Math.min(tickLeftOffset, lineLeftOffset + lineWidth - tickWidth); - tick.getStyle().setLeft(tickLeftOffset, Unit.PX); - tick.getStyle().setVisibility(Visibility.VISIBLE); - } - - // Hide unused ticks - for (int i = (numTicks + 1); i < tickElements.size(); i++) { - tickElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all ticks - for (Element elem : tickElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Draw the markers. - */ - private void drawMarkers() { - if (isAttached() && isMinMaxInitialized()) { - int numMarkers = markers.size(); - // Draw the markers - int lineWidth = lineElement.getOffsetWidth(); - if (numMarkers > 0) { - // Create the markers or make them visible - Element lastMarkerElement = null; - int lastMarkerOffsetLeft = 0; - for (int i = 0; i < numMarkers; i++) { - Marker marker = markers.get(i); - Element markerElem = null; - if (i < markerElements.size()) { - markerElem = markerElements.get(i); - } else { // Create the new markes - markerElem = DOM.createDiv(); - markerElem.getStyle().setPosition(Position.ABSOLUTE); - markerElem.getStyle().setDisplay(Display.NONE); - DOM.appendChild(getElement(), markerElem); - markerElements.add(markerElem); - } - if (enabled) { - markerElem.setPropertyString("className", "gwt-SliderBar-mark"); - } else { - markerElem.setPropertyString("className", "gwt-SliderBar-mark gwt-SliderBar-mark-disabled"); - } - // Position the marker and make it visible - markerElem.getStyle().setVisibility(Visibility.HIDDEN); - markerElem.getStyle().setProperty("display", ""); - double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); - int markerWidth = markerElem.getOffsetWidth(); - int markerLeftOffset = lineLeftOffset + (int) markerLinePosition - (markerWidth / 2); - markerLeftOffset = Math.min(markerLeftOffset, lineLeftOffset + lineWidth - markerWidth); - markerElem.getStyle().setLeft(markerLeftOffset, Unit.PX); - markerElem.getStyle().setVisibility(Visibility.VISIBLE); - // change class of last marker if it is too close to the one just created - if (lastMarkerOffsetLeft > 0 && (markerLeftOffset-lastMarkerOffsetLeft) <= markerWidth) { - if (lastMarkerElement != null) { - lastMarkerElement.setPropertyString("className", "gwt-SliderBar-mark-tooClose"); - lastMarkerElement.getStyle().setLeft(markerLeftOffset-10, Unit.PX); - } - } - lastMarkerElement = markerElem; - lastMarkerOffsetLeft = markerLeftOffset; - } - - // Hide unused markers - for (int i = numMarkers; i < markerElements.size(); i++) { - markerElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all markers - for (Element elem : markerElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Draw the marker labels. - */ - private void drawMarkerLabels() { - if (isAttached() && isMinMaxInitialized()) { - int numMarkers = markers.size(); - // Draw the marker labels - int lineWidth = lineElement.getOffsetWidth(); - if (numMarkers > 0) { - Element lastMarkerLabel = null; - Marker lastMarker = null; - int lastMarkerLabelOffsetLeft = 0; - // Create the labels or make them visible - for (int i = 0; i < numMarkers; i++) { - Marker marker = markers.get(i); - Element label = null; - if (i < markerLabelElements.size()) { - label = markerLabelElements.get(i); - } else { // Create the new label - label = DOM.createDiv(); - label.getStyle().setPosition(Position.ABSOLUTE); - label.getStyle().setDisplay(Display.NONE); - if (enabled) { - label.setPropertyString("className", "gwt-SliderBar-markerlabel"); - } else { - label.setPropertyString("className", "gwt-SliderBar-markerlabel-disabled"); - } - DOM.appendChild(getElement(), label); - markerLabelElements.add(label); - } - - // Set the marker label text - label.getStyle().setVisibility(Visibility.HIDDEN); - label.getStyle().setProperty("display", ""); - label.setPropertyString("innerHTML", marker.name); - - // Move to the left so the label width is not clipped by the shell - label.getStyle().setLeft(0, Unit.PX); - - // Position the label and make it visible - double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); - int labelWidth = label.getOffsetWidth(); - int labelLeftOffset = lineLeftOffset + (int) markerLinePosition - (labelWidth / 2); - labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); - - label.getStyle().setLeft(labelLeftOffset, Unit.PX); - label.getStyle().setVisibility(Visibility.VISIBLE); - - // hide last marker label if it is too close to the one just created - if (lastMarkerLabelOffsetLeft > 0 && (labelLeftOffset-lastMarkerLabelOffsetLeft) <= 20) { - if (lastMarkerLabel != null && lastMarker != null) { - lastMarkerLabel.setPropertyString("className", "gwt-SliderBar-markerlabel-tooClose"); - lastMarkerLabel.setPropertyString("innerHTML", lastMarker.name); // no space for more - int currentMarkerOffsetLeft = labelLeftOffset; - try { - currentMarkerOffsetLeft = Integer.parseInt(markerElements.get(i).getStyle().getLeft().replace("px", "")); - } catch (NumberFormatException ex) { - // ignore, can't do anything - } - lastMarkerLabel.getStyle().setLeft(currentMarkerOffsetLeft-8, Unit.PX); - } - } - lastMarker = marker; - lastMarkerLabel = label; - lastMarkerLabelOffsetLeft = labelLeftOffset; - } - - // Hide unused labels - for (int i = (numMarkers + 1); i < markerLabelElements.size(); i++) { - markerLabelElements.get(i).getStyle().setDisplay(Display.NONE); - } - } else { // Hide all labels - for (Element elem : markerLabelElements) { - elem.getStyle().setDisplay(Display.NONE); - } - } - } - } - - /** - * Highlight this widget. - */ - private void highlight() { - String styleName = getStylePrimaryName(); - getElement().setPropertyString("className", styleName + " " + styleName + "-focused"); - } - - /** - * Reset the progress to constrain the progress to the current range and redraw the knob as needed. - */ - private synchronized void resetCurrentValue(boolean fireEvent) { - setCurrentValue(getCurrentValue(), fireEvent); - } - - /** - * Slide the knob to a new location. - * - * @param event - * the mouse event - */ - private void slideKnob(Event event) { - // Adding scrollLeft to adjust the position, if the user had scrolled with the lower scroll bar - int x = event.getClientX() + Window.getScrollLeft(); - if (x > 0) { - int lineWidth = lineElement.getOffsetWidth(); - int lineLeft = lineElement.getAbsoluteLeft(); - double percent = (double) (x - lineLeft) / lineWidth * 1.0; - setCurrentValue(getTotalRange() * percent + minValue, true); - } - } - - /** - * Start sliding the knob. - * - * @param highlight - * true to change the style - * @param fireEvent - * true to fire the event - */ - private void startSliding(boolean highlight, boolean fireEvent) { - if (highlight) { - lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-sliding"); - knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob gwt-SliderBar-knob-sliding"); - knobImage.setResource(images.sliderSliding()); - } - } - - /** - * Stop sliding the knob. - * - * @param unhighlight - * true to change the style - * @param fireEvent - * true to fire the event - */ - private void stopSliding(boolean unhighlight, boolean fireEvent) { - if (unhighlight) { - lineElement.setPropertyString("className", "gwt-SliderBar-line"); - knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob"); - knobImage.setResource(images.slider()); - } - } - - /** - * Unhighlight this widget. - */ - private void unhighlight() { - getElement().setPropertyString("className", getStylePrimaryName()); - } - - @Override - public void onResize() { - redraw(); - } - - public void clearMarkers() { - markers.clear(); - } - - public boolean addMarker(String markerName, Double markerPosition) { - return markers.add(new Marker(markerName, markerPosition)); - } - - public boolean setMarker(String markerName, Double markerPosition) { - Marker marker = findMarkerByName(markerName); - if (marker != null) { - marker.position = markerPosition; - return true; - } - return false; - } - - public boolean removeMarker(String markerName) { - Marker marker = findMarkerByName(markerName); - if (marker != null) { - return markers.remove(marker); - } - return false; - } - - public Iterator getMarkers() { - return markers.iterator(); - } - - private Marker findMarkerByName(String markerName) { - for (Marker marker : markers) { - if (marker.name.equals(markerName)) { - return marker; - } - } - return null; - } -} +package com.sap.sse.gwt.client.controls.slider; + +/* + * Copyright 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +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.Element; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.dom.client.Style.Position; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.dom.client.Style.Visibility; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.event.logical.shared.HasValueChangeHandlers; +import com.google.gwt.event.logical.shared.ValueChangeEvent; +import com.google.gwt.event.logical.shared.ValueChangeHandler; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.resources.client.ClientBundle; +import com.google.gwt.resources.client.CssResource; +import com.google.gwt.resources.client.CssResource.NotStrict; +import com.google.gwt.resources.client.ImageResource; +import com.google.gwt.user.client.DOM; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.Window; +import com.google.gwt.user.client.ui.FocusPanel; +import com.google.gwt.user.client.ui.HasValue; +import com.google.gwt.user.client.ui.Image; +import com.google.gwt.user.client.ui.RequiresResize; +import com.sap.sse.common.Util; + +/** + * A widget that allows the user to select a value within a range of possible values using a sliding bar that responds + * to mouse events. + * + *

    Keyboard Events

    + *

    + * SliderBar listens for the following key events. Holding down a key will repeat the action until the key is released. + *

      + *
    • left arrow - shift left one step
    • + *
    • right arrow - shift right one step
    • + *
    • ctrl+left arrow - jump left 10% of the distance
    • + *
    • ctrl+right arrow - jump right 10% of the distance
    • + *
    • home - jump to min value
    • + *
    • end - jump to max value
    • + *
    • space - jump to middle value
    • + *
    + *

    + * + *

    CSS Style Rules

    + *
      + *
    • .gwt-SliderBar-shell { primary style }
    • + *
    • .gwt-SliderBar-shell-focused { primary style when focused }
    • + *
    • .gwt-SliderBar-shell gwt-SliderBar-line { the line that the knob moves along }
    • + *
    • .gwt-SliderBar-shell gwt-SliderBar-line-sliding { the line that the knob moves along when sliding }
    • + *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob { the sliding knob }
    • + *
    • .gwt-SliderBar-shell .gwt-SliderBar-knob-sliding { the sliding knob when sliding }
    • + *
    • + * .gwt-SliderBar-shell .gwt-SliderBar-tick { the ticks along the line }
    • + *
    • .gwt-SliderBar-shell .gwt-SliderBar-ticklabel { the text labels along the line }
    • + *
    + */ +public class SliderBar extends FocusPanel implements RequiresResize, HasValue, HasValueChangeHandlers { + /** + * A formatter used to format the labels displayed in the widget. + */ + public static interface LabelFormatter { + /** + * Generate the text to display in each label based on the label's value. + * + * Override this method to change the text displayed within the SliderBar. + * + * @param slider + * the Slider bar + * @param value + * the value the label displays + * @return the text to display for the label + */ + String formatLabel(SliderBar slider, Double value, Double previousValue); + } + + /** + * A {@link ClientBundle} that provides images for {@link SliderBar}. + */ + public static interface SliderBarImages extends ClientBundle { + public static final SliderBarImages INSTANCE = GWT.create(SliderBarImages.class); + + /** + * An image used for the sliding knob. + * + * @return a prototype of this image + */ + @Source("slider_blue.png") + ImageResource slider(); + + /** + * An image used for the disabled sliding knob. + * + * @return a prototype of this image + */ + @Source("slider_blue.png") + ImageResource sliderDisabled(); + + /** + * An image used for the sliding knob while sliding. + * + * @return a prototype of this image + */ + @Source("slider_blue.png") + ImageResource sliderSliding(); + + @NotStrict + @Source("SliderBar.css") + CssResource sliderBarCss(); + } + + /** + * The timer used to continue to shift the knob as the user holds down one of the left/right arrow keys. Only IE + * auto-repeats, so we just keep catching the events. + */ + private class KeyTimer extends Timer { + /** + * A bit indicating that this is the first run. + */ + private boolean firstRun = true; + + /** + * The delay between shifts, which shortens as the user holds down the button. + */ + private int repeatDelay = 30; + + /** + * A bit indicating whether we are shifting to a higher or lower value. + */ + private boolean shiftRight = false; + + /** + * The number of steps to shift with each press. + */ + private int multiplier = 1; + + /** + * This method will be called when a timer fires. Override it to implement the timer's logic. + */ + @Override + public void run() { + // Highlight the knob on first run + if (firstRun) { + firstRun = false; + startSliding(true, false); + } + + // Slide the slider bar + if (shiftRight) { + setCurrentValue(curValue + multiplier * stepSize); + } else { + setCurrentValue(curValue - multiplier * stepSize); + } + + // Repeat this timer until cancelled by keyup event + schedule(repeatDelay); + } + + /** + * Schedules a timer to elapse in the future. + * + * @param delayMillis + * how long to wait before the timer elapses, in milliseconds + * @param shiftRight + * whether to shift up or not + * @param multiplier + * the number of steps to shift + */ + public void schedule(int delayMillis, boolean shiftRight, int multiplier) { + firstRun = true; + this.shiftRight = shiftRight; + this.multiplier = multiplier; + super.schedule(delayMillis); + } + } + + /** + * The current value. + */ + protected Double curValue; + + /** + * The knob that slides across the line. + */ + protected Image knobImage = new Image(); + + /** + * The timer used to continue to shift the knob if the user holds down a key. + */ + private KeyTimer keyTimer = new KeyTimer(); + + /** + * The elements used to display labels above the ticks. + */ + protected List tickLabelElements = new ArrayList(); + + /** + * The elements used to display the marker labels. + */ + protected List markerLabelElements = new ArrayList(); + + /** + * The formatter used to generate label text. + */ + protected LabelFormatter tickLabelFormatter; + + /** + * The line that the knob moves over. + */ + protected Element lineElement; + + /** + * The offset between the edge of the shell and the line. + */ + protected int lineLeftOffset = 0; + + /** + * The maximum slider value. + */ + protected Double maxValue; + + /** + * The minimum slider value. + */ + protected Double minValue; + + /** + * The number of labels to show. + */ + private int numTickLabels = 0; + + /** + * The number of tick marks to show. + */ + protected int numTicks = 0; + + /** + * A bit indicating whether or not we are currently sliding the slider bar due to keyboard events. + */ + private boolean slidingKeyboard = false; + + /** + * A bit indicating whether or not we are currently sliding the slider bar due to mouse events. + */ + private boolean slidingMouse = false; + + /** + * A bit indicating whether or not the slider is enabled + */ + protected boolean enabled = true; + + /** + * The images used with the sliding bar. + */ + private SliderBarImages images; + + /** + * The size of the increments between knob positions. + */ + protected double stepSize; + + /** + * The elements used to display tick marks, which are the vertical lines along the slider bar. + */ + protected List tickElements = new ArrayList(); + + /** + * The elements used to display additional markers on the slider bar. + */ + protected List markerElements = new ArrayList(); + + private List markers = new ArrayList(); + + private class Marker { + String name; + Double position; + + public Marker(String name, Double position) { + super(); + this.name = name; + this.position = position; + } + } + + /** + * Create a slider bar. + */ + public SliderBar() { + this(null, null, null); + } + + /** + * Create a slider bar. + * + * @param minValue + * the minimum value in the range + * @param maxValue + * the maximum value in the range + */ + public SliderBar(double minValue, double maxValue) { + this(minValue, maxValue, null); + } + + /** + * Create a slider bar. + * + * @param minValue + * the minimum value in the range + * @param maxValue + * the maximum value in the range + * @param tickLabelFormatter + * the label formatter + */ + public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter) { + this(minValue, maxValue, tickLabelFormatter, SliderBarImages.INSTANCE); + } + + /** + * Create a slider bar. + * + * @param minValue + * the minimum value in the range + * @param maxValue + * the maximum value in the range + * @param tickLabelFormatter + * the label formatter + * @param images + * the images to use for the slider + */ + public SliderBar(Double minValue, Double maxValue, LabelFormatter tickLabelFormatter, SliderBarImages images) { + super(); + images.sliderBarCss().ensureInjected(); + this.minValue = minValue; + this.maxValue = maxValue; + this.images = images; + setLabelFormatter(tickLabelFormatter); + + // Create the outer shell + getElement().getStyle().setPosition(Position.RELATIVE); + setStyleName("gwt-SliderBar-shell"); + + // Create the line + lineElement = DOM.createDiv(); + DOM.appendChild(getElement(), lineElement); + lineElement.getStyle().setPosition(Position.ABSOLUTE); + lineElement.setPropertyString("className", "gwt-SliderBar-line"); + + // Create the knob + knobImage.setResource(images.slider()); + Element knobElement = knobImage.getElement(); + DOM.appendChild(getElement(), knobElement); + knobElement.getStyle().setPosition(Position.ABSOLUTE); + knobElement.setPropertyString("className", "gwt-SliderBar-knob"); + + sinkEvents(Event.MOUSEEVENTS | Event.KEYEVENTS | Event.FOCUSEVENTS); + + // workaround to render properly when parent Widget does not + // implement ProvidesResize since DOM doesn't provide element + // height and width until onModuleLoad() finishes. + Scheduler.get().scheduleDeferred(new ScheduledCommand() { + @Override + public void execute() { + onResize(); + } + }); + } + + public boolean isMinMaxInitialized() { + return minValue != null && maxValue != null; + } + + public HandlerRegistration addValueChangeHandler(ValueChangeHandler handler) { + return addHandler(handler, ValueChangeEvent.getType()); + } + + /** + * Return the current value. + * + * @return the current value + */ + public Double getCurrentValue() { + return curValue; + } + + /** + * Return the label formatter. + * + * @return the label formatter + */ + public LabelFormatter getLabelFormatter() { + return tickLabelFormatter; + } + + /** + * Return the max value. + * + * @return the max value + */ + public Double getMaxValue() { + return maxValue; + } + + /** + * Return the minimum value. + * + * @return the minimum value + */ + public Double getMinValue() { + return minValue; + } + + /** + * Return the number of labels. + * + * @return the number of labels + */ + public int getNumLabels() { + return numTickLabels; + } + + /** + * Return the number of ticks. + * + * @return the number of ticks + */ + public int getNumTicks() { + return numTicks; + } + + /** + * Return the step size. + * + * @return the step size + */ + public double getStepSize() { + return stepSize; + } + + /** + * Return the total range between the minimum and maximum values. + * + * @return the total range + */ + public double getTotalRange() { + if (minValue == null || maxValue == null || minValue > maxValue) { + return 0; + } else { + return maxValue - minValue; + } + } + + public Double getValue() { + return curValue; + } + + /** + * @return Gets whether this widget is enabled + */ + public boolean isEnabled() { + return enabled; + } + + /** + * Listen for events that will move the knob. + * + * @param event + * the event that occurred + */ + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (enabled) { + switch (DOM.eventGetType(event)) { + // Unhighlight and cancel keyboard events + case Event.ONBLUR: + keyTimer.cancel(); + if (slidingMouse) { + DOM.releaseCapture(getElement()); + slidingMouse = false; + slideKnob(event); + stopSliding(true, true); + } else if (slidingKeyboard) { + slidingKeyboard = false; + stopSliding(true, true); + } + unhighlight(); + break; + + // Highlight on focus + case Event.ONFOCUS: + highlight(); + break; + + // Mousewheel events + case Event.ONMOUSEWHEEL: + int velocityY = event.getMouseWheelVelocityY(); + event.preventDefault(); + if (velocityY > 0) { + shiftRight(1); + } else { + shiftLeft(1); + } + break; + + // Shift left or right on key press + case Event.ONKEYDOWN: + if (!slidingKeyboard) { + int multiplier = 1; + if (event.getCtrlKey()) { + multiplier = (int) (getTotalRange() / stepSize / 10); + } + + switch (event.getKeyCode()) { + case KeyCodes.KEY_HOME: + event.preventDefault(); + setCurrentValue(minValue); + break; + case KeyCodes.KEY_END: + event.preventDefault(); + setCurrentValue(maxValue); + break; + case KeyCodes.KEY_LEFT: + event.preventDefault(); + slidingKeyboard = true; + startSliding(false, true); + shiftLeft(multiplier); + keyTimer.schedule(400, false, multiplier); + break; + case KeyCodes.KEY_RIGHT: + event.preventDefault(); + slidingKeyboard = true; + startSliding(false, true); + shiftRight(multiplier); + keyTimer.schedule(400, true, multiplier); + break; + case 32: + event.preventDefault(); + setCurrentValue(minValue + getTotalRange() / 2); + break; + } + } + break; + // Stop shifting on key up + case Event.ONKEYUP: + keyTimer.cancel(); + if (slidingKeyboard) { + slidingKeyboard = false; + stopSliding(true, true); + } + break; + + // Mouse Events + case Event.ONMOUSEDOWN: + setFocus(true); + slidingMouse = true; + DOM.setCapture(getElement()); + startSliding(true, true); + event.preventDefault(); + slideKnob(event); + break; + case Event.ONMOUSEUP: + if (slidingMouse) { + DOM.releaseCapture(getElement()); + slidingMouse = false; + slideKnob(event); + stopSliding(true, true); + } + break; + case Event.ONMOUSEMOVE: + if (slidingMouse) { + slideKnob(event); + } + break; + } + } + } + + /** + * This method is called when the dimensions of the parent element change. Subclasses should override this method as + * needed. + * + * @param width + * the new client width of the element + * @param height + * the new client height of the element + */ + public void onResize(int width, int height) { + // Center the line in the shell + int lineWidth = lineElement.getOffsetWidth(); + lineLeftOffset = (width / 2) - (lineWidth / 2); + lineElement.getStyle().setLeft(lineLeftOffset, Unit.PX); + + // Draw the other components + drawTickLabels(); + drawTicks(); + drawMarkers(); + drawMarkerLabels(); + drawKnob(); + } + + /** + * Redraw the progress bar when something changes the layout. + */ + public void redraw() { + if (isAttached()) { + int width = getElement().getClientWidth(); + int height = getElement().getClientHeight(); + onResize(width, height); + } + } + + /** + * Set the current value and fire the onValueChange event. + * + * @param curValue + * the current value + */ + public synchronized void setCurrentValue(Double curValue) { + setCurrentValue(curValue, true); + } + + /** + * Set the current value and optionally fire the onValueChange event. + * + * @param curValue + * the current value + * @param fireEvent + * fire the onValue change event if true + */ + public synchronized void setCurrentValue(Double curValue, boolean fireEvent) { + // Confine the value to the range + if (!isMinMaxInitialized() || curValue == null) { + return; + } + this.curValue = Math.max(minValue, Math.min(maxValue, curValue)); + double remainder = (this.curValue - minValue) % stepSize; + this.curValue -= remainder; + // Go to next step if more than halfway there + if ((remainder > (stepSize / 2)) && ((this.curValue + stepSize) <= maxValue)) { + this.curValue += stepSize; + } + // Redraw the knob + drawKnob(); + // Fire the ValueChangeEvent if the value actually changed + if (fireEvent && !curValue.equals(this.curValue)) { + ValueChangeEvent.fire(this, this.curValue); + } + } + + /** + * Sets whether this widget is enabled. + * + * @param enabled + * true to enable the widget, false to disable it + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if (enabled) { + knobImage.setResource(images.slider()); + lineElement.setPropertyString("className", "gwt-SliderBar-line"); + } else { + knobImage.setResource(images.sliderDisabled()); + lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-disabled"); + } + redraw(); + } + + /** + * Set the label formatter. + * + * @param labelFormatter + * the label formatter + */ + public void setLabelFormatter(LabelFormatter labelFormatter) { + this.tickLabelFormatter = labelFormatter; + } + + /** + * Set the max value. + * + * @param maxValue + * the current value + */ + public void setMaxValue(Double maxValue, boolean fireEvent) { + if (!Util.equalsWithNull(maxValue, this.maxValue)) { + this.maxValue = maxValue; + onMinMaxValueChanged(fireEvent); + } + } + + /** + * Set the minimum value. + * + * @param minValue + * the current value + */ + public void setMinValue(Double minValue, boolean fireEvent) { + if (!Util.equalsWithNull(minValue, this.minValue)) { + this.minValue = minValue; + onMinMaxValueChanged(fireEvent); + } + } + + /** + * Set the minimum and maximum value + * + * @param minValue + * the current value for min + * @param maxValue + * the current valuefor max + */ + public void setMinAndMaxValue(Double minValue, Double maxValue, boolean fireEvent) { + boolean changed = false; + if (!Util.equalsWithNull(minValue, this.minValue)) { + this.minValue = minValue; + changed = true; + } + if (!Util.equalsWithNull(maxValue, this.maxValue)) { + this.maxValue = maxValue; + changed = true; + } + if (changed) { + onMinMaxValueChanged(fireEvent); + } + } + + /** + * Handle value changes of min and/or max value + */ + protected void onMinMaxValueChanged(boolean fireEvent) { + redraw(); + resetCurrentValue(fireEvent); + } + + /** + * Set the number of tick labels to show on the line. Tick labels indicate the value of the slider at that point. + * Use this method to enable tick labels. + * + * If you set the number of tick labels equal to the total range divided by the step size, you will get a properly + * aligned "jumping" effect where the knob jumps between tick labels. + * + * Note that the number of tick labels displayed will be one more than the number you specify, so specify 1 labels + * to show labels on either end of the line. In other words, numTickLabels is really the number of slots between the + * labels. + * + * setNumTickLabels(0) will disable the labels. + * + * @param numTickLabels + * the number of tick labels to show + */ + public void setNumTickLabels(int numTickLabels) { + this.numTickLabels = numTickLabels; + drawTickLabels(); + } + + /** + * Set the number of ticks to show on the line. A tick is a vertical line that represents a division of the overall + * line. Use this method to enable ticks. + * + * If you set the number of ticks equal to the total range divided by the step size, you will get a properly aligned + * "jumping" effect where the knob jumps between ticks. + * + * Note that the number of ticks displayed will be one more than the number you specify, so specify 1 tick to show + * ticks on either end of the line. In other words, numTicks is really the number of slots between the ticks. + * + * setNumTicks(0) will disable ticks. + * + * @param numTicks + * the number of ticks to show + */ + public void setNumTicks(int numTicks) { + this.numTicks = numTicks; + drawTicks(); + } + + /** + * Set the step size. + * + * @param stepSize + * the current value + */ + public void setStepSize(double stepSize, boolean fireEvent) { + this.stepSize = stepSize; + resetCurrentValue(fireEvent); + } + + public void setValue(Double value) { + setCurrentValue(value, false); + } + + public void setValue(Double value, boolean fireEvent) { + setCurrentValue(value, fireEvent); + } + + /** + * Shift to the left (smaller value). + * + * @param numSteps + * the number of steps to shift + */ + public void shiftLeft(int numSteps) { + setCurrentValue(getCurrentValue() - numSteps * stepSize); + } + + /** + * Shift to the right (greater value). + * + * @param numSteps + * the number of steps to shift + */ + public void shiftRight(int numSteps) { + setCurrentValue(getCurrentValue() + numSteps * stepSize); + } + + /** + * Format the label to display above the ticks + * + * Override this method in a subclass to customize the format. By default, this method returns the integer portion + * of the value. + * + * @param value + * the value at the label + * @return the text to put in the label + */ + protected String formatTickLabel(Double value, Double previousValue) { + if (tickLabelFormatter != null) { + return tickLabelFormatter.formatLabel(this, value, previousValue); + } else { + return (int) (10 * value) / 10.0 + ""; + } + } + + /** + * Get the percentage of the knob's position relative to the size of the line. The return value will be between 0.0 + * and 1.0. + * + * @return the current percent complete + */ + protected double getKnobPercent() { + // If we have no range + if (maxValue <= minValue) { + return 0; + } + + // Calculate the relative progress + double percent = (curValue - minValue) / (maxValue - minValue); + return Math.max(0.0, Math.min(1.0, percent)); + } + + /** + * This method is called immediately after a widget becomes attached to the browser's document. + */ + @Override + protected void onLoad() { + // Reset the position attribute of the parent element + getElement().getStyle().setPosition(Position.RELATIVE); + } + + /** + * Draw the knob where it is supposed to be relative to the line. + */ + protected void drawKnob() { + if (isAttached() && isMinMaxInitialized()) { + // Move the knob to the correct position + Element knobElement = knobImage.getElement(); + int lineWidth = lineElement.getOffsetWidth(); + int knobWidth = knobElement.getOffsetWidth(); + int knobLeftOffset = (int) (lineLeftOffset + (getKnobPercent() * lineWidth) - (knobWidth / 2)); + knobLeftOffset = Math.min(knobLeftOffset, lineLeftOffset + lineWidth - (knobWidth / 2) - 1); + knobElement.getStyle().setLeft(knobLeftOffset, Unit.PX); + } + } + + /** + * Draw the labels along the line. + */ + protected void drawTickLabels() { + if (isAttached() && isMinMaxInitialized()) { + // Draw the tick labels + int lineWidth = lineElement.getOffsetWidth(); + if (numTickLabels > 0) { + // Create the labels or make them visible + Double previousValue = null; + for (int i = 0; i <= numTickLabels; i++) { + Element label = null; + if (i < tickLabelElements.size()) { + label = tickLabelElements.get(i); + } else { // Create the new label + label = DOM.createDiv(); + label.getStyle().setPosition(Position.ABSOLUTE); + label.getStyle().setDisplay(Display.NONE); + if (enabled) { + label.setPropertyString("className", "gwt-SliderBar-ticklabel"); + } else { + label.setPropertyString("className", "gwt-SliderBar-ticklabel-disabled"); + } + DOM.appendChild(getElement(), label); + tickLabelElements.add(label); + } + + // Set the label text + double value = minValue + (getTotalRange() * i / numTickLabels); + label.getStyle().setVisibility(Visibility.HIDDEN); + label.getStyle().setProperty("display", ""); + label.setPropertyString("innerHTML", formatTickLabel(value, previousValue)); + + // Move to the left so the label width is not clipped by the shell + label.getStyle().setLeft(0, Unit.PX); + + // Position the label and make it visible + int labelWidth = label.getOffsetWidth(); + int labelLeftOffset = lineLeftOffset + (lineWidth * i / numTickLabels) - (labelWidth / 2); + labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); + labelLeftOffset = Math.max(labelLeftOffset, lineLeftOffset); + label.getStyle().setLeft(labelLeftOffset, Unit.PX); + label.getStyle().setVisibility(Visibility.VISIBLE); + + previousValue = value; + } + + // Hide unused labels + for (int i = (numTickLabels + 1); i < tickLabelElements.size(); i++) { + tickLabelElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all labels + for (Element elem : tickLabelElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Draw the tick along the line. + */ + protected void drawTicks() { + if (isAttached() && isMinMaxInitialized()) { + // Draw the ticks + int lineWidth = lineElement.getOffsetWidth(); + if (numTicks > 0) { + // Create the ticks or make them visible + for (int i = 0; i <= numTicks; i++) { + Element tick = null; + if (i < tickElements.size()) { + tick = tickElements.get(i); + } else { // Create the new tick + tick = DOM.createDiv(); + tick.getStyle().setPosition(Position.ABSOLUTE); + tick.getStyle().setDisplay(Display.NONE); + DOM.appendChild(getElement(), tick); + tickElements.add(tick); + } + if (enabled) { + tick.setPropertyString("className", "gwt-SliderBar-tick"); + } else { + tick.setPropertyString("className", "gwt-SliderBar-tick gwt-SliderBar-tick-disabled"); + } + // Position the tick and make it visible + tick.getStyle().setVisibility(Visibility.HIDDEN); + tick.getStyle().setProperty("display", ""); + int tickWidth = tick.getOffsetWidth(); + int tickLeftOffset = lineLeftOffset + (lineWidth * i / numTicks) - (tickWidth / 2); + tickLeftOffset = Math.min(tickLeftOffset, lineLeftOffset + lineWidth - tickWidth); + tick.getStyle().setLeft(tickLeftOffset, Unit.PX); + tick.getStyle().setVisibility(Visibility.VISIBLE); + } + + // Hide unused ticks + for (int i = (numTicks + 1); i < tickElements.size(); i++) { + tickElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all ticks + for (Element elem : tickElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Draw the markers. + */ + private void drawMarkers() { + if (isAttached() && isMinMaxInitialized()) { + int numMarkers = markers.size(); + // Draw the markers + int lineWidth = lineElement.getOffsetWidth(); + if (numMarkers > 0) { + // Create the markers or make them visible + Element lastMarkerElement = null; + int lastMarkerOffsetLeft = 0; + for (int i = 0; i < numMarkers; i++) { + Marker marker = markers.get(i); + Element markerElem = null; + if (i < markerElements.size()) { + markerElem = markerElements.get(i); + } else { // Create the new markes + markerElem = DOM.createDiv(); + markerElem.getStyle().setPosition(Position.ABSOLUTE); + markerElem.getStyle().setDisplay(Display.NONE); + DOM.appendChild(getElement(), markerElem); + markerElements.add(markerElem); + } + if (enabled) { + markerElem.setPropertyString("className", "gwt-SliderBar-mark"); + } else { + markerElem.setPropertyString("className", "gwt-SliderBar-mark gwt-SliderBar-mark-disabled"); + } + // Position the marker and make it visible + markerElem.getStyle().setVisibility(Visibility.HIDDEN); + markerElem.getStyle().setProperty("display", ""); + double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); + int markerWidth = markerElem.getOffsetWidth(); + int markerLeftOffset = lineLeftOffset + (int) markerLinePosition - (markerWidth / 2); + markerLeftOffset = Math.min(markerLeftOffset, lineLeftOffset + lineWidth - markerWidth); + markerElem.getStyle().setLeft(markerLeftOffset, Unit.PX); + markerElem.getStyle().setVisibility(Visibility.VISIBLE); + // change class of last marker if it is too close to the one just created + if (lastMarkerOffsetLeft > 0 && (markerLeftOffset-lastMarkerOffsetLeft) <= markerWidth) { + if (lastMarkerElement != null) { + lastMarkerElement.setPropertyString("className", "gwt-SliderBar-mark-tooClose"); + lastMarkerElement.getStyle().setLeft(markerLeftOffset-10, Unit.PX); + } + } + lastMarkerElement = markerElem; + lastMarkerOffsetLeft = markerLeftOffset; + } + + // Hide unused markers + for (int i = numMarkers; i < markerElements.size(); i++) { + markerElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all markers + for (Element elem : markerElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Draw the marker labels. + */ + private void drawMarkerLabels() { + if (isAttached() && isMinMaxInitialized()) { + int numMarkers = markers.size(); + // Draw the marker labels + int lineWidth = lineElement.getOffsetWidth(); + if (numMarkers > 0) { + Element lastMarkerLabel = null; + Marker lastMarker = null; + int lastMarkerLabelOffsetLeft = 0; + // Create the labels or make them visible + for (int i = 0; i < numMarkers; i++) { + Marker marker = markers.get(i); + Element label = null; + if (i < markerLabelElements.size()) { + label = markerLabelElements.get(i); + } else { // Create the new label + label = DOM.createDiv(); + label.getStyle().setPosition(Position.ABSOLUTE); + label.getStyle().setDisplay(Display.NONE); + DOM.appendChild(getElement(), label); + markerLabelElements.add(label); + } + if (enabled) { + label.setPropertyString("className", "gwt-SliderBar-markerlabel"); + } else { + label.setPropertyString("className", "gwt-SliderBar-markerlabel-disabled"); + } + + // Set the marker label text + label.getStyle().setVisibility(Visibility.HIDDEN); + label.getStyle().setProperty("display", ""); + label.setPropertyString("innerHTML", marker.name); + + // Move to the left so the label width is not clipped by the shell + label.getStyle().setLeft(0, Unit.PX); + + // Position the label and make it visible + double markerLinePosition = (marker.position - minValue) * lineWidth / getTotalRange(); + int labelWidth = label.getOffsetWidth(); + int labelLeftOffset = lineLeftOffset + (int) markerLinePosition - (labelWidth / 2); + labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); + + label.getStyle().setLeft(labelLeftOffset, Unit.PX); + label.getStyle().setVisibility(Visibility.VISIBLE); + + // hide last marker label if it is too close to the one just created + if (lastMarkerLabelOffsetLeft > 0 && (labelLeftOffset-lastMarkerLabelOffsetLeft) <= 20) { + if (lastMarkerLabel != null && lastMarker != null) { + lastMarkerLabel.setPropertyString("className", "gwt-SliderBar-markerlabel-tooClose"); + lastMarkerLabel.setPropertyString("innerHTML", lastMarker.name); // no space for more + int currentMarkerOffsetLeft = labelLeftOffset; + try { + currentMarkerOffsetLeft = Integer.parseInt(markerElements.get(i).getStyle().getLeft().replace("px", "")); + } catch (NumberFormatException ex) { + // ignore, can't do anything + } + lastMarkerLabel.getStyle().setLeft(currentMarkerOffsetLeft-8, Unit.PX); + } + } + lastMarker = marker; + lastMarkerLabel = label; + lastMarkerLabelOffsetLeft = labelLeftOffset; + } + + // Hide unused labels + for (int i = (numMarkers + 1); i < markerLabelElements.size(); i++) { + markerLabelElements.get(i).getStyle().setDisplay(Display.NONE); + } + } else { // Hide all labels + for (Element elem : markerLabelElements) { + elem.getStyle().setDisplay(Display.NONE); + } + } + } + } + + /** + * Highlight this widget. + */ + private void highlight() { + String styleName = getStylePrimaryName(); + getElement().setPropertyString("className", styleName + " " + styleName + "-focused"); + } + + /** + * Reset the progress to constrain the progress to the current range and redraw the knob as needed. + */ + private synchronized void resetCurrentValue(boolean fireEvent) { + setCurrentValue(getCurrentValue(), fireEvent); + } + + /** + * Slide the knob to a new location. + * + * @param event + * the mouse event + */ + private void slideKnob(Event event) { + // Adding scrollLeft to adjust the position, if the user had scrolled with the lower scroll bar + int x = event.getClientX() + Window.getScrollLeft(); + if (x > 0) { + int lineWidth = lineElement.getOffsetWidth(); + int lineLeft = lineElement.getAbsoluteLeft(); + double percent = (double) (x - lineLeft) / lineWidth * 1.0; + setCurrentValue(getTotalRange() * percent + minValue, true); + } + } + + /** + * Start sliding the knob. + * + * @param highlight + * true to change the style + * @param fireEvent + * true to fire the event + */ + private void startSliding(boolean highlight, boolean fireEvent) { + if (highlight) { + lineElement.setPropertyString("className", "gwt-SliderBar-line gwt-SliderBar-line-sliding"); + knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob gwt-SliderBar-knob-sliding"); + knobImage.setResource(images.sliderSliding()); + } + } + + /** + * Stop sliding the knob. + * + * @param unhighlight + * true to change the style + * @param fireEvent + * true to fire the event + */ + private void stopSliding(boolean unhighlight, boolean fireEvent) { + if (unhighlight) { + lineElement.setPropertyString("className", "gwt-SliderBar-line"); + knobImage.getElement().setPropertyString("className", "gwt-SliderBar-knob"); + knobImage.setResource(images.slider()); + } + } + + /** + * Unhighlight this widget. + */ + private void unhighlight() { + getElement().setPropertyString("className", getStylePrimaryName()); + } + + @Override + public void onResize() { + redraw(); + } + + public void clearMarkers() { + markers.clear(); + } + + public boolean addMarker(String markerName, Double markerPosition) { + return markers.add(new Marker(markerName, markerPosition)); + } + + public boolean setMarker(String markerName, Double markerPosition) { + Marker marker = findMarkerByName(markerName); + if (marker != null) { + marker.position = markerPosition; + return true; + } + return false; + } + + public boolean removeMarker(String markerName) { + Marker marker = findMarkerByName(markerName); + if (marker != null) { + return markers.remove(marker); + } + return false; + } + + public Iterator getMarkers() { + return markers.iterator(); + } + + private Marker findMarkerByName(String markerName) { + for (Marker marker : markers) { + if (marker.name.equals(markerName)) { + return marker; + } + } + return null; + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java index 99dafe676bc..8383b5a6c60 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeSlider.java @@ -26,7 +26,9 @@ public class TimeSlider extends SliderBar { private List calculatedTimeTicks; private boolean isZoomed; - + + private int visibleLabelsInterval = 1; + private final int TICKCOUNT = 10; public TimeSlider() { @@ -84,7 +86,7 @@ public class TimeSlider extends SliderBar { int tickLeftOffset = lineLeftOffset + (int) pos - (tickWidth / 2); tickLeftOffset = Math.min(tickLeftOffset, lineLeftOffset + lineWidth - tickWidth); tick.getStyle().setLeft(tickLeftOffset, Unit.PX); - tick.getStyle().setVisibility(Visibility.VISIBLE); + tick.getStyle().setVisibility(isTickInVisibleRange(tickPosition) ? Visibility.VISIBLE : Visibility.HIDDEN); } } @@ -106,7 +108,7 @@ public class TimeSlider extends SliderBar { int lineWidth = lineElement.getOffsetWidth(); // Create the labels or make them visible Double previousValue = null; - for (int i = 0; i < calculatedTimeTicks.size(); i++) { + for (int i = 0, ticksInVisibleRange = 0; i < calculatedTimeTicks.size(); i++) { TickPosition tickPosition = calculatedTimeTicks.get(i); Element label = null; if (i < tickLabelElements.size()) { @@ -142,7 +144,8 @@ public class TimeSlider extends SliderBar { labelLeftOffset = Math.min(labelLeftOffset, lineLeftOffset + lineWidth - labelWidth); labelLeftOffset = Math.max(labelLeftOffset, lineLeftOffset); label.getStyle().setLeft(labelLeftOffset, Unit.PX); - label.getStyle().setVisibility(Visibility.VISIBLE); + boolean visible = isTickInVisibleRange(tickPosition) && ticksInVisibleRange++ % visibleLabelsInterval == 0; + label.getStyle().setVisibility(visible ? Visibility.VISIBLE : Visibility.HIDDEN); } previousValue = value; } @@ -152,6 +155,11 @@ public class TimeSlider extends SliderBar { tickLabelElements.get(i).getStyle().setDisplay(Display.NONE); } } + + private boolean isTickInVisibleRange(TickPosition tickPosition) { + long tickValue = tickPosition.getPosition().getTime(); + return tickValue >= minValue && tickValue <= maxValue; + } public void clearMarkersAndLabelsAndTicks() { clearMarkers(); @@ -245,4 +253,10 @@ public class TimeSlider extends SliderBar { public void setZoomed(boolean isZoomed) { this.isZoomed = isZoomed; } -} + + @Override + public void onResize() { + visibleLabelsInterval = (TICKCOUNT / Math.max(getOffsetWidth() / 35, 1)) + 1; + super.onResize(); + } +} \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java index 8a28bd12fb2..735989a2776 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/slider/TimeTicksCalculator.java @@ -1,246 +1,247 @@ -package com.sap.sse.gwt.client.controls.slider; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -/** - * This calculator emulates the way the HighCharts charting library is computing the chart ticks - * in case of a x-axis based on time values. - */ -public class TimeTicksCalculator { - - public TimeTicksCalculator() { - } - - /** - * Get a normalized tick interval for dates. Returns a configuration object with unit range (interval), count and - * name. Used to prepare data for getTimeTicks. Previously this logic was part of getTimeTicks, but as getTimeTicks - * now runs of segments in stock charts, the normalizing logic was extracted in order to prevent it for running over - * again for each segment having the same interval. - */ - public NormalizedInterval normalizeTimeTickInterval(long tickInterval) { - TimeUnits[] units = TimeUnits.values(); - - TimeUnits unit = units[units.length - 1]; // default unit is years - long interval = unit.unitInMs; - int[] multiples = unit.allowedMultiples; - int count; - - // loop through the units to find the one that best fits the tickInterval - for (int i = 0; i < units.length; i++) { - unit = units[i]; - interval = unit.unitInMs; - multiples = unit.allowedMultiples; - - if (i + 1 < units.length) { - // lessThan is in the middle between the highest multiple and the next unit. - long lessThan = (interval * multiples[multiples.length - 1] + units[i + 1].unitInMs) / 2; - - // break and keep the current unit - if (tickInterval <= lessThan) { - break; - } - } - } - - // prevent 2.5 years intervals, though 25, 250 etc. are allowed - if (interval == TimeUnits.YEAR.unitInMs && tickInterval < 5 * interval) { - multiples = new int[] { 1, 2, 5 }; - } - - // get the count - count = (int) normalizeTickInterval(tickInterval / interval, multiples, 1); - - return new NormalizedInterval(unit.name(), interval, count); - } - - /** - * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 - * - * @param {Number} interval - * @param {Array} multiples - * @param {Number} magnitude - * @param {Object} options - */ - public long normalizeTickInterval(long interval, int[] multiples, int magnitude) { - long normalized; - - // round to a tenfold of 1, 2, 2.5 or 5 - normalized = interval / magnitude; - - // normalize the interval to the nearest multiple - for (int i = 0; i < multiples.length; i++) { - interval = multiples[i]; - int addedValue = i + 1 < multiples.length ? multiples[i + 1] : multiples[i]; - if (normalized <= (multiples[i] + addedValue) / 2) { - break; - } - } - - // multiply back to the correct magnitude - interval *= magnitude; - - return interval; - } - - private long makeTime(int year, int month) { - return makeTime(year, month, 1, 0, 0, 0); - } - - private long makeTime(int year, int month, int date) { - return makeTime(year, month, date, 0, 0, 0); - } - - @SuppressWarnings("deprecation") - private long makeTime(int year, int month, int date, int hours, int minutes, int seconds) { - return new Date(year, month, date, hours, minutes, seconds).getTime(); - } - - /** - * Set the tick positions to a time unit that makes sense, for example on the first of each month or on every - * Monday. Return an array with the time positions. Used in datetime axes as well as for grouping data on a datetime - * axis. - * - * @param {Object} normalizedInterval The interval in axis values (ms) and the count - * @param {Number} min The minimum in axis values - * @param {Number} max The maximum in axis values - * @param {Number} startOfWeek - */ - @SuppressWarnings("deprecation") - public List calculateTimeTicks(NormalizedInterval normalizedInterval, long min, long max, - long startOfWeek) { - List tickPositions = new ArrayList(); - int i; - boolean useUTC = true; - int minYear = 0; // used in months and years as a basis for Date.UTC() - Date minDate = new Date(min); - long interval = normalizedInterval.unitRange; - int count = (int) normalizedInterval.count; - - - if (interval >= TimeUnits.SECOND.unitInMs) { // second - minDate.setTime(min - (min % 1000)); - minDate.setSeconds(interval >= TimeUnits.MINUTE.unitInMs ? 0 : count - * Math.round(minDate.getSeconds() / count)); - } - - if (interval >= TimeUnits.MINUTE.unitInMs) { // minute - minDate.setMinutes(interval >= TimeUnits.HOUR.unitInMs ? 0 : count - * Math.round(minDate.getMinutes() / count)); - } - - if (interval >= TimeUnits.HOUR.unitInMs) { // hour - minDate.setHours(interval >= TimeUnits.DAY.unitInMs ? 0 : count * Math.round(minDate.getHours() / count)); - } - - if (interval >= TimeUnits.DAY.unitInMs) { // day - minDate.setDate(interval >= TimeUnits.MONTH.unitInMs ? 1 : count * Math.round(minDate.getDate() / count)); - } - - if (interval >= TimeUnits.MONTH.unitInMs) { // month - minDate.setMonth(interval >= TimeUnits.YEAR.unitInMs ? 0 : count * Math.round(minDate.getMonth() + 1 / count) - 1); - minYear = minDate.getYear() + 1900; - } - - if (interval >= TimeUnits.YEAR.unitInMs) { // year - minYear -= minYear % count; - minDate.setYear(minYear - 1900); - } - - // week is a special case that runs outside the hierarchy - if (interval == TimeUnits.WEEK.unitInMs) { - // get start of current week, independent of count - long value = minDate.getDate() - minDate.getDay() + startOfWeek; - minDate.setDate((int) value); - } - - // get tick positions - i = 1; - minYear = minDate.getYear() + 1900; - int minMonth = minDate.getMonth() + 1; - int minDateDate = minDate.getDate(); - long time = minDate.getTime(); - - // iterate and add tick positions at appropriate values - while (time < max) { - if(time >= min) - tickPositions.add(new TickPosition(time)); - - // if the interval is years, use Date.UTC to increase years - if (interval == TimeUnits.YEAR.unitInMs) { - time = makeTime(minYear + i * count, 0); - - // if the interval is months, use Date.UTC to increase months - } else if (interval == TimeUnits.MONTH.unitInMs) { - time = makeTime(minYear, minMonth + i * count); - - // if we're using global time, the interval is not fixed as it jumps - // one hour at the DST crossover - } else if (!useUTC && (interval == TimeUnits.DAY.unitInMs || interval == TimeUnits.WEEK.unitInMs)) { - time = makeTime(minYear, minMonth, minDateDate + i * count * (interval == TimeUnits.DAY.unitInMs ? 1 : 7)); - - // else, the interval is fixed and we use simple addition - } else { - time += interval * count; - } - - i++; - } - - // push the last time - if(time <= max) - tickPositions.add(new TickPosition(time)); - - return tickPositions; - } - - public class NormalizedInterval { - public String unitName; - public long unitRange; - public int count; - - public NormalizedInterval(String unitName, long unitRange, int count) { - this.unitName = unitName; - this.unitRange = unitRange; - this.count = count; - } - - @Override - public String toString() { - return "NormalizedInterval [unitName=" + unitName + ", unitRange=" + unitRange + ", count=" + count + "]"; - } - } - - public class TickPosition { - private Date position; - - public TickPosition(long time) { - this.position = new Date(time); - } - - public Date getPosition() { - return position; - } - } - - enum TimeUnits { - MILLISECOND(1, new int[] { 1, 2, 5, 10, 20, 25, 50, 100, 200, 500 }), - SECOND(1000, new int[] { 1, 2, 5, 10, 15, 30 }), - MINUTE(60 * 1000, new int[] { 1, 2, 5, 10, 15, 30 }), - HOUR(60 * 60 * 1000, new int[] { 1, 2, 3, 4, 6, 8, 12 }), - DAY(24 * 3600000l, new int[] { 1, 2 }), - WEEK(7 * 24 * 3600000l, new int[] { 1, 2 }), - MONTH(30 * 24 * 3600000l, new int[] { 1, 2, 3, 4, 6 }), - YEAR(31556952000l, new int[] {}); - - int[] allowedMultiples; - - long unitInMs; - - TimeUnits(long unitInMs, int[] allowedMultiples) { - this.unitInMs = unitInMs; - this.allowedMultiples = allowedMultiples; - } - } -} +package com.sap.sse.gwt.client.controls.slider; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * This calculator emulates the way the HighCharts charting library is computing the chart ticks + * in case of a x-axis based on time values. + */ +public class TimeTicksCalculator { + + public TimeTicksCalculator() { + } + + /** + * Get a normalized tick interval for dates. Returns a configuration object with unit range (interval), count and + * name. Used to prepare data for getTimeTicks. Previously this logic was part of getTimeTicks, but as getTimeTicks + * now runs of segments in stock charts, the normalizing logic was extracted in order to prevent it for running over + * again for each segment having the same interval. + */ + public NormalizedInterval normalizeTimeTickInterval(long tickInterval) { + TimeUnits[] units = TimeUnits.values(); + + TimeUnits unit = units[units.length - 1]; // default unit is years + long interval = unit.unitInMs; + int[] multiples = unit.allowedMultiples; + int count; + + // loop through the units to find the one that best fits the tickInterval + for (int i = 0; i < units.length; i++) { + unit = units[i]; + interval = unit.unitInMs; + multiples = unit.allowedMultiples; + + if (i + 1 < units.length) { + // lessThan is in the middle between the highest multiple and the next unit. + double lessThan = (interval * multiples[multiples.length - 1] + units[i + 1].unitInMs) / 2; + + // break and keep the current unit + if (tickInterval <= lessThan) { + break; + } + } + } + + // prevent 2.5 years intervals, though 25, 250 etc. are allowed + if (interval == TimeUnits.YEAR.unitInMs && tickInterval < 5 * interval) { + multiples = new int[] { 1, 2, 5 }; + } + + // get the count + double temp = ((double) tickInterval) / ((double) interval); + count = (int) normalizeTickInterval(temp, multiples, 1); + + return new NormalizedInterval(unit.name(), interval, count); + } + + /** + * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 + * + * @param {Number} interval + * @param {Array} multiples + * @param {Number} magnitude + * @param {Object} options + */ + public long normalizeTickInterval(double interval, int[] multiples, int magnitude) { + double normalized; + + // round to a tenfold of 1, 2, 2.5 or 5 + normalized = interval / magnitude; + + // normalize the interval to the nearest multiple + for (int i = 0; i < multiples.length; i++) { + interval = multiples[i]; + int addedValue = i + 1 < multiples.length ? multiples[i + 1] : multiples[i]; + if (normalized <= (multiples[i] + addedValue) / 2) { + break; + } + } + + // multiply back to the correct magnitude + interval *= magnitude; + + return Math.round(interval); + } + + private long makeTime(int year, int month) { + return makeTime(year, month, 1, 0, 0, 0); + } + + private long makeTime(int year, int month, int date) { + return makeTime(year, month, date, 0, 0, 0); + } + + @SuppressWarnings("deprecation") + private long makeTime(int year, int month, int date, int hours, int minutes, int seconds) { + return new Date(year, month, date, hours, minutes, seconds).getTime(); + } + + /** + * Set the tick positions to a time unit that makes sense, for example on the first of each month or on every + * Monday. Return an array with the time positions. Used in datetime axes as well as for grouping data on a datetime + * axis. + * + * @param {Object} normalizedInterval The interval in axis values (ms) and the count + * @param {Number} min The minimum in axis values + * @param {Number} max The maximum in axis values + * @param {Number} startOfWeek + */ + @SuppressWarnings("deprecation") + public List calculateTimeTicks(NormalizedInterval normalizedInterval, long min, long max, + long startOfWeek) { + List tickPositions = new ArrayList(); + int i; + boolean useUTC = true; + int minYear = 0; // used in months and years as a basis for Date.UTC() + Date minDate = new Date(min); + long interval = normalizedInterval.unitRange; + int count = (int) normalizedInterval.count; + + + if (interval >= TimeUnits.SECOND.unitInMs) { // second + minDate.setTime(min - (min % 1000)); + minDate.setSeconds(interval >= TimeUnits.MINUTE.unitInMs ? 0 : count + * Math.round(minDate.getSeconds() / count)); + } + + if (interval >= TimeUnits.MINUTE.unitInMs) { // minute + minDate.setMinutes(interval >= TimeUnits.HOUR.unitInMs ? 0 : count + * Math.round(minDate.getMinutes() / count)); + } + + if (interval >= TimeUnits.HOUR.unitInMs) { // hour + minDate.setHours(interval >= TimeUnits.DAY.unitInMs ? 0 : count * Math.round(minDate.getHours() / count)); + } + + if (interval >= TimeUnits.DAY.unitInMs) { // day + minDate.setDate(interval >= TimeUnits.MONTH.unitInMs ? 1 : count * Math.round(minDate.getDate() / count)); + } + + if (interval >= TimeUnits.MONTH.unitInMs) { // month + minDate.setMonth(interval >= TimeUnits.YEAR.unitInMs ? 0 : count * Math.round(minDate.getMonth() + 1 / count) - 1); + minYear = minDate.getYear() + 1900; + } + + if (interval >= TimeUnits.YEAR.unitInMs) { // year + minYear -= minYear % count; + minDate.setYear(minYear - 1900); + } + + // week is a special case that runs outside the hierarchy + if (interval == TimeUnits.WEEK.unitInMs) { + // get start of current week, independent of count + long value = minDate.getDate() - minDate.getDay() + startOfWeek; + minDate.setDate((int) value); + } + + // get tick positions + i = 1; + minYear = minDate.getYear() + 1900; + int minMonth = minDate.getMonth() + 1; + int minDateDate = minDate.getDate(); + long time = minDate.getTime(); + + // iterate and add tick positions at appropriate values + while (time < max) { + if(time >= min) + tickPositions.add(new TickPosition(time)); + + // if the interval is years, use Date.UTC to increase years + if (interval == TimeUnits.YEAR.unitInMs) { + time = makeTime(minYear + i * count, 0); + + // if the interval is months, use Date.UTC to increase months + } else if (interval == TimeUnits.MONTH.unitInMs) { + time = makeTime(minYear, minMonth + i * count); + + // if we're using global time, the interval is not fixed as it jumps + // one hour at the DST crossover + } else if (!useUTC && (interval == TimeUnits.DAY.unitInMs || interval == TimeUnits.WEEK.unitInMs)) { + time = makeTime(minYear, minMonth, minDateDate + i * count * (interval == TimeUnits.DAY.unitInMs ? 1 : 7)); + + // else, the interval is fixed and we use simple addition + } else { + time += interval * count; + } + + i++; + } + + // push the last time + if(time <= max) + tickPositions.add(new TickPosition(time)); + + return tickPositions; + } + + public class NormalizedInterval { + public String unitName; + public long unitRange; + public int count; + + public NormalizedInterval(String unitName, long unitRange, int count) { + this.unitName = unitName; + this.unitRange = unitRange; + this.count = count; + } + + @Override + public String toString() { + return "NormalizedInterval [unitName=" + unitName + ", unitRange=" + unitRange + ", count=" + count + "]"; + } + } + + public class TickPosition { + private Date position; + + public TickPosition(long time) { + this.position = new Date(time); + } + + public Date getPosition() { + return position; + } + } + + enum TimeUnits { + MILLISECOND(1l, 1, 2, 5, 10, 20, 25, 50, 100, 200, 500), + SECOND(1000l, 1, 2, 5, 10, 15, 30), + MINUTE(60 * 1000l, 1, 2, 5, 10, 15, 30), + HOUR(60 * 60 * 1000l, 1, 2, 3, 4, 6, 8, 12), + DAY(24 * 3600000l, 1, 2), + WEEK(7 * 24 * 3600000l, 1, 2), + MONTH(30 * 24 * 3600000l, 1, 2, 3, 4, 6), + YEAR(31556952000l); + + int[] allowedMultiples; + + long unitInMs; + + TimeUnits(long unitInMs, int... allowedMultiples) { + this.unitInMs = unitInMs; + this.allowedMultiples = allowedMultiples; + } + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css index a15efd6a2c7..a30d6ccc9a7 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/resources/css/CommonControls.css @@ -169,8 +169,7 @@ body ::-webkit-scrollbar-track { .gwt-SplitLayoutPanel-EastToggleButton-Panel { position: absolute; - bottom: 77px; - margin-left: -35px; + bottom: -10px; } .gwt-SplitLayoutPanel-EastToggleButton { @@ -181,9 +180,14 @@ body ::-webkit-scrollbar-track { -moz-transform: rotate(-90deg); -o-transform: rotate(-90deg); -ms-transform: rotate(-90deg); + transform: rotate(-90deg); + -webkit-transform-origin: 0 0; + -moz-transform-origin: 0 0; + -o-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; padding-right: 25px; padding-left: 30px; - margin-left: -15px; border-bottom: 1px solid #cecece; border-right: 1px solid #cecece; border-left: 1px solid #cecece; diff --git a/java/com.sap.sse.security.common/pom.xml b/java/com.sap.sse.security.common/pom.xml index 063d3938b30..45259c58bad 100755 --- a/java/com.sap.sse.security.common/pom.xml +++ b/java/com.sap.sse.security.common/pom.xml @@ -1,32 +1,32 @@ - - - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - com.sap.sse.security.common - eclipse-plugin - - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - - - - + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sse.security.common + eclipse-plugin + + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + + diff --git a/java/com.sap.sse.shared.android/META-INF/MANIFEST.MF b/java/com.sap.sse.shared.android/META-INF/MANIFEST.MF index e7ad05c7f81..179cf981850 100644 --- a/java/com.sap.sse.shared.android/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.shared.android/META-INF/MANIFEST.MF @@ -7,4 +7,6 @@ Bundle-Vendor: SAP Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Require-Bundle: com.sap.sse.common Export-Package: com.sap.sse.concurrent, + com.sap.sse.shared.media, + com.sap.sse.shared.media.impl, com.sap.sse.util.impl diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/media/ImageDescriptor.java b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/ImageDescriptor.java similarity index 90% rename from java/com.sap.sse.common/src/com/sap/sse/common/media/ImageDescriptor.java rename to java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/ImageDescriptor.java index 67ace752668..6bc37ff7450 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/media/ImageDescriptor.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/ImageDescriptor.java @@ -1,4 +1,4 @@ -package com.sap.sse.common.media; +package com.sap.sse.shared.media; import com.sap.sse.common.Util.Pair; diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/media/MediaDescriptor.java b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/MediaDescriptor.java similarity index 91% rename from java/com.sap.sse.common/src/com/sap/sse/common/media/MediaDescriptor.java rename to java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/MediaDescriptor.java index 6f777b7b6be..e6298e67dd1 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/media/MediaDescriptor.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/MediaDescriptor.java @@ -1,10 +1,11 @@ -package com.sap.sse.common.media; +package com.sap.sse.shared.media; import java.io.Serializable; import java.net.URL; import java.util.Locale; import com.sap.sse.common.TimePoint; +import com.sap.sse.common.media.MimeType; /** * A common media interface for all kinds of media like images or videos. diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/media/MediaUtils.java b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/MediaUtils.java similarity index 98% rename from java/com.sap.sse.common/src/com/sap/sse/common/media/MediaUtils.java rename to java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/MediaUtils.java index 987f90a77e5..2ea747ed4ee 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/media/MediaUtils.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/MediaUtils.java @@ -1,4 +1,4 @@ -package com.sap.sse.common.media; +package com.sap.sse.shared.media; import java.io.IOException; import java.net.URL; @@ -18,6 +18,7 @@ import javax.imageio.stream.ImageInputStream; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.media.MimeType; public class MediaUtils { private static final Logger logger = Logger.getLogger(MediaUtils.class.getName()); diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/media/VideoDescriptor.java b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/VideoDescriptor.java similarity index 87% rename from java/com.sap.sse.common/src/com/sap/sse/common/media/VideoDescriptor.java rename to java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/VideoDescriptor.java index 1f21c990afa..411138ae1af 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/media/VideoDescriptor.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/VideoDescriptor.java @@ -1,4 +1,4 @@ -package com.sap.sse.common.media; +package com.sap.sse.shared.media; import java.net.URL; diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/media/WithImages.java b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/WithImages.java similarity index 95% rename from java/com.sap.sse.common/src/com/sap/sse/common/media/WithImages.java rename to java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/WithImages.java index ade9f8c6791..450f7e0fa20 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/media/WithImages.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/WithImages.java @@ -1,4 +1,5 @@ -package com.sap.sse.common.media; +package com.sap.sse.shared.media; + public interface WithImages { /** diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/media/WithMedia.java b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/WithMedia.java similarity index 91% rename from java/com.sap.sse.common/src/com/sap/sse/common/media/WithMedia.java rename to java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/WithMedia.java index ac433dc3bbf..e929dfc1f39 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/media/WithMedia.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/WithMedia.java @@ -1,4 +1,4 @@ -package com.sap.sse.common.media; +package com.sap.sse.shared.media; import java.util.List; diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/media/WithVideos.java b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/WithVideos.java similarity index 95% rename from java/com.sap.sse.common/src/com/sap/sse/common/media/WithVideos.java rename to java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/WithVideos.java index 5706f49757b..c6e411117a6 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/media/WithVideos.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/WithVideos.java @@ -1,4 +1,5 @@ -package com.sap.sse.common.media; +package com.sap.sse.shared.media; + public interface WithVideos { diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/media/AbstractMediaDescriptor.java b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl/AbstractMediaDescriptor.java similarity index 95% rename from java/com.sap.sse.common/src/com/sap/sse/common/media/AbstractMediaDescriptor.java rename to java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl/AbstractMediaDescriptor.java index 5621e82192a..c1fb05a6431 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/media/AbstractMediaDescriptor.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl/AbstractMediaDescriptor.java @@ -1,4 +1,4 @@ -package com.sap.sse.common.media; +package com.sap.sse.shared.media.impl; import java.io.Serializable; import java.net.URL; @@ -8,6 +8,8 @@ import java.util.Set; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; +import com.sap.sse.common.media.MimeType; +import com.sap.sse.shared.media.MediaDescriptor; /** * Common media data for media items diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/media/ImageDescriptorImpl.java b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl/ImageDescriptorImpl.java similarity index 91% rename from java/com.sap.sse.common/src/com/sap/sse/common/media/ImageDescriptorImpl.java rename to java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl/ImageDescriptorImpl.java index 48f7c10896b..147a958a2d9 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/media/ImageDescriptorImpl.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl/ImageDescriptorImpl.java @@ -1,9 +1,11 @@ -package com.sap.sse.common.media; +package com.sap.sse.shared.media.impl; import java.net.URL; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.media.MimeType; +import com.sap.sse.shared.media.ImageDescriptor; public class ImageDescriptorImpl extends AbstractMediaDescriptor implements ImageDescriptor { diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/media/VideoDescriptorImpl.java b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl/VideoDescriptorImpl.java similarity index 89% rename from java/com.sap.sse.common/src/com/sap/sse/common/media/VideoDescriptorImpl.java rename to java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl/VideoDescriptorImpl.java index ce8d112252e..0be8e8db323 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/media/VideoDescriptorImpl.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/shared/media/impl/VideoDescriptorImpl.java @@ -1,8 +1,10 @@ -package com.sap.sse.common.media; +package com.sap.sse.shared.media.impl; import java.net.URL; import com.sap.sse.common.TimePoint; +import com.sap.sse.common.media.MimeType; +import com.sap.sse.shared.media.VideoDescriptor; public class VideoDescriptorImpl extends AbstractMediaDescriptor implements VideoDescriptor { private static final long serialVersionUID = 2651747912466590862L; diff --git a/java/com.sap.sse/src/com/sap/sse/ServerInfo.java b/java/com.sap.sse/src/com/sap/sse/ServerInfo.java index 7e2360aa33c..6300fab7489 100644 --- a/java/com.sap.sse/src/com/sap/sse/ServerInfo.java +++ b/java/com.sap.sse/src/com/sap/sse/ServerInfo.java @@ -3,6 +3,8 @@ package com.sap.sse; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; +import java.util.logging.Level; +import java.util.logging.Logger; /** * A class providing static information about the running server. @@ -10,14 +12,16 @@ import java.io.FileReader; * */ public class ServerInfo { + private static final Logger logger = Logger.getLogger(ServerInfo.class.getName()); + public static String getBuildVersion() { String version = "Unknown or Development (" + ServerStartupConstants.SERVER_NAME + ")"; File versionfile = new File(ServerStartupConstants.JETTY_HOME + File.separator + "version.txt"); if (versionfile.exists()) { - try { - version = new BufferedReader(new FileReader(versionfile)).readLine(); + try (BufferedReader bufferedReader = new BufferedReader(new FileReader(versionfile))) { + version = bufferedReader.readLine(); } catch (Exception ex) { - /* ignore */ + logger.log(Level.WARNING, "Error trying to obtain version info", ex); } } return version; diff --git a/java/com.sap.sse/src/com/sap/sse/util/IdentityWrapper.java b/java/com.sap.sse/src/com/sap/sse/util/IdentityWrapper.java new file mode 100644 index 00000000000..2391349d401 --- /dev/null +++ b/java/com.sap.sse/src/com/sap/sse/util/IdentityWrapper.java @@ -0,0 +1,34 @@ +package com.sap.sse.util; + +import java.io.Serializable; + +/** + * Implements {@link Object#equals(Object)} and {@link Object#hashCode()} based on the wrapped object's + * identity, no matter what the object itself defines as {@code equals} and {@code hashCode}. + * + * @author Axel Uhl (D043530) + * + */ +public class IdentityWrapper implements Serializable { + private static final long serialVersionUID = 3514488568026067341L; + private final T t; + + public IdentityWrapper(T t) { + this.t = t; + } + + @Override + public int hashCode() { + return System.identityHashCode(t); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof IdentityWrapper) ? ((IdentityWrapper) obj).t == this.t : obj == t; + } + + @Override + public String toString() { + return "IdentityWrapper for "+t; + } +} diff --git a/java/org.json.simple/pom.xml b/java/org.json.simple/pom.xml index 375032ca86a..5b842829851 100644 --- a/java/org.json.simple/pom.xml +++ b/java/org.json.simple/pom.xml @@ -1,40 +1,40 @@ - - - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - org.json.simple - 1.1.0-SNAPSHOT - eclipse-plugin - - - - org.eclipse.tycho - tycho-compiler-plugin - ${tycho-version} - - 1.7 - 1.7 - - - - org.eclipse.tycho - tycho-source-plugin - ${tycho-version} - - - plugin-source - generate-sources - - plugin-source - - - - - - - + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + org.json.simple + 1.1.0-SNAPSHOT + eclipse-plugin + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + 1.7 + 1.7 + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + generate-sources + + plugin-source + + + + + + + diff --git a/java/pom.xml b/java/pom.xml index 9ae21eaa32d..fc283d0da09 100755 --- a/java/pom.xml +++ b/java/pom.xml @@ -134,8 +134,8 @@ com.sap.sailing.autoload com.sap.sailing.feature.p2build com.sap.sailing.polars - com.sap.sailing.polars.datamining - com.sap.sailing.polars.datamining.shared + com.sap.sailing.polars.datamining + com.sap.sailing.polars.datamining.shared com.sap.sailing.polars.test + org.apache.maven.plugins maven-dependency-plugin diff --git a/mobile/pom.xml b/mobile/pom.xml index 069aa8e6d97..e3fc75b072d 100644 --- a/mobile/pom.xml +++ b/mobile/pom.xml @@ -16,13 +16,13 @@ - + ../../java/com.sap.sailing.www/apps @@ -32,21 +32,21 @@ - - - android - android - 4.4.2_r4 - provided - + + + android + android + 4.4.2_r4 + provided + - - - android.support - compatibility-v4 - 19 - - + + + android.support + compatibility-v4 + 19 + + @@ -55,90 +55,90 @@ com.jayway.maven.plugins.android.generation2 android-maven-plugin - 3.9.0-rc.1 - - - 19 - - + 3.9.0-rc.1 + + + 19 + + true - - org.apache.maven.plugins - maven-dependency-plugin - 2.8 - - - copy-installed - install - - copy - - - - - ${project.groupId} - ${project.artifactId} - ${project.version} - ${project.packaging} - - - ${additional-deployment-dir} - true - - - - - - org.apache.maven.plugins - maven-antrun-plugin - 1.3 - - - generate-version - install - - run - - - - - - - - - - - - - - - maven-clean-plugin - 2.5 - - - - ${additional-deployment-dir} - - ${project.build.finalName}.apk - ${additional-deployment-version-file} - - - *.gitignore - - false - - - - - - org.apache.maven.plugins - maven-compiler-plugin - - 1.7 - 1.7 - - + + org.apache.maven.plugins + maven-dependency-plugin + 2.8 + + + copy-installed + install + + copy + + + + + ${project.groupId} + ${project.artifactId} + ${project.version} + ${project.packaging} + + + ${additional-deployment-dir} + true + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.3 + + + generate-version + install + + run + + + + + + + + + + + + + + + maven-clean-plugin + 2.5 + + + + ${additional-deployment-dir} + + ${project.build.finalName}.apk + ${additional-deployment-version-file} + + + *.gitignore + + false + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.7 + 1.7 + + diff --git a/pom.xml b/pom.xml index 946b2122559..77c807e02f7 100755 --- a/pom.xml +++ b/pom.xml @@ -1,68 +1,68 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" + xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + 4.0.0 - com.sap.sailing - workspace - 1.0.0-SNAPSHOT - pom + com.sap.sailing + workspace + 1.0.0-SNAPSHOT + pom - - java - + + java + - - - - with-mobile - - true - - - mobile - - - - - with-ios - - ios - - - + + + + with-mobile + + true + + + mobile + + + + + with-ios + + ios + + + - - UTF-8 - UTF-8 - - - - - equinox-sdk-3.9 - p2 - http://download.eclipse.org/releases/luna/ - - - - sailing-server-maven - http://maven.sapsailing.com/maven - + + sailing-server-maven + http://maven.sapsailing.com/maven + - - central - http://repo1.maven.org/maven2 - + + central + http://repo1.maven.org/maven2 + - - uni-luebeck - http://www.itm.uni-luebeck.de/projects/maven/releases - - + + uni-luebeck + http://www.itm.uni-luebeck.de/projects/maven/releases + + diff --git a/wiki/Log-File-Analysis.md b/wiki/Log-File-Analysis.md index 1cfbc853a9b..eddce4b9329 100644 --- a/wiki/Log-File-Analysis.md +++ b/wiki/Log-File-Analysis.md @@ -64,33 +64,103 @@ This uses the SetEnvIf module, tries to match an IP address at the start of the ### Amazon EC2 Elastic Load Balancer (ELB) Logs -An Amazon ELB can be configured to write log files to the S3 storage. The general format is [explained here](http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/access-log-collection.html#access-log-entry-format). It contains in particular the client IP where the request originated and can tell timing parameters for request forwarding and processing that otherwise would not be available. It seems a good idea to always capture these logs for archiving purposes. They end up on an S3 bucket, and by default we use `sapsailing-access-logs`. +An Amazon ELB can be configured to write log files to the S3 storage. The general format is [explained here](http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/access-log-collection.html#access-log-entry-format). It contains in particular the client IP where the request originated and can tell timing parameters for request forwarding and processing that otherwise would not be available. + +The format for these logs is + +
    +timestamp elb client:port backend:port request_processing_time backend_processing_time response_processing_time elb_status_code backend_status_code received_bytes sent_bytes "request" "user_agent" ssl_cipher ssl_protocol
    +
    + +It seems a good idea to always capture these logs for archiving purposes. They end up on an S3 bucket, and by default we use `sapsailing-access-logs`. Content can be synced from there to a local directory using the following command: ``s3cmd sync s3://sapsailing-access-logs/elb-access-logs ./elb-access-logs/`` +Our central web server at `www.sapsailing.com` does this periodically every day, sync'ing the logs to `/var/log/old/elb-access-logs/`. The corresponding cron job is defined in `/etc/cron.daily/syncEC2ElbLogs` which is a script also found in git in the `configuration/` folder. + +### Broken or Partly Missing Logs and How We Recover + +During a few events we unfortunately failed to create proper Apache log files according to the above rules for scenarios using a load-balanced setup ("ELB scenario"). In some cases, the Apache log format was missing the referrer URL. This is easy to patch into the files because we collected the logs on a per-server basis and we knew which server ran which event. For example, for Kieler Woche 2015 and Travemünder Woche 2015 we needed to insert `kielerwoche2015.sapsailing.com` and `tw2015.sapsailing.com`, respectively, to match the general log format used for analysis. + +The other problem, as described already above, is that of the original client IP address. Before we encountered how to log the original client IP from the `X-Forwarded-For` header field, only the ELB's IP addresses were logged in the Apache logs. While counting the correct number of hits is possible with such logs, they don't reveal the true number of unique visitors. They do, however, contain a valid referrer URL, telling us which leaderboards and races were the most popular. + +In case we had an ELB log active, storing the Amazon ELB logs to an S3 bucket, we were able to convert those with a script stored in our git at `configuration/convertELBLogToApacheFormat` into our general Apache log format. This allows us to count the unique visitors for those events, but the ELB logs don't contain the referrer URL, so we patch that to always be the plain event URL, such as `http://kielerwoche2015.sapsailing.com`. Therefore, while these converted logs tell the correct number of unique visitors and the correct number of hits, they don't allow for an analysis of the most popular leaderboards and races. + +We hope that these partially-logged events remain the exception and that future events are logged homogeneously according to the rules above. The following naming conventions have been established under `/var/log/old` that shall allow the automatic log analyzers to identify the log file contents by file name patterns: + + - `access_log*`: A proper Apache log file with virtual host name as the first field, followed by the original client's IP address and a valid referrer URL as the second-to-last field + - `elb-origin-access_log*`: An Apache log file whose origin IP addresses are usually only the ELB IP addresses, therefore not allowing for unique visitor analysis + - `bundesliga2015_elb_access_log.gz` and `tw2015_elb_access_log.gz`: ELB log files converted to Apache format, appended in order of ascending time stamps, prefixed with the virtual host name as the first field, using the original client IP address which allows for unique visitor analysis, and the virtual host name used again as generic referrer URL + - `original-access_log*`: Apache log file without leading virtual host name field, also usually only with ELB IP addresses instead of an original client IP; not good for any log file analysis without further conversion, but kept for reference purposes + ## Automatic Log File Rotation to /var/log/old - - describe how logrotate is used and automatically configured during instance start-up and where the logs go +When an EC2 server is launched from its Amazon Machine Image (AMI), the /etc/init.d/sailing script is executed with the "start" parameter. This script is really just a link to `/home/sailing/code/configuration/sailing` which comes from the git version checked out to the workspace at `/home/sailing/code`. This script patches the file `/etc/logrotate.d/httpd` such that when log rotation happens, the existence of the directory `/var/log/old/$SERVER_NAME/$SERVER_IP` is ensured and the log files are copied there. + +This way, logs end up in a per server-name and per server-ip directory. + +The general log rotation rules (size, time, compression, etc.) is governed by `/etc/logrotate.conf`. ## Analysis Tools ### goaccess - - mention specific parameters and .goaccess config file required to parse our format +The file `/root/.goaccessrc` on `www.sapsailing.com` looks as follows: + +``` +color_scheme 1 +date_format %d/%b/%Y +time_format %H:%M:%S +log_format %^ %h %^[%d:%^] "%r" %s %b "%R" "%u" +log_file STDIN +``` + +Still, specific parameters are required for `goaccess` in order to parse our regular Apache logs that have the virtual host name as their first field: + +`goaccess --no-global-config -a -f /var/log/httpd/access_log` + +The unique visitors that goaccess lists are based on the common definition that counts requests as originating from the same visitor if they happened on the same day with equal user agent and IP address. ### apachetop +The simple command `apachetop` shows the traffic on the Apache httpd server running on the host where the command is invoked, based on the access log file in the usual location `/var/log/httpd/access_log`. + ### AWStats - - specific log collection approach configured in /etc/awstats/*.conf script +We use AWStats to analyze our Apache access log files and produce per-month reports at [http://awstats.sapsailing.com](http://awstats.sapsailing.com/awstats/awstats.pl?output=main&config=www.sapsailing.com&framename=index). The username is `awstats`. Ask axel.uhl@sap.com or stefan.lacher@sap.com for the password. - - extension for per-virtual-host hit count +AWStats report production is controlled by three things: a cron job hooked up by `/etc/cron.weekly/awstats` which is basically a one-liner launching the `awstats` command like this: - - configuration of central apache to present results (awstats.sapsailing.com) +`exec /usr/share/awstats/tools/awstats_updateall.pl now -configdir="/etc/awstats" -awstatsprog="/usr/share/awstats/wwwroot/cgi-bin/awstats.pl"` + +and a configuration file located at `/etc/awstats/awstats.www.sapsailing.com.conf` which describes in its `LogFile` directive the filename pattern to use for collecting the log files that shall be analyzed. Currently, the file name pattern is this: + +`/var/log/httpd/access_log /var/log/old/access_log-???????? /var/log/old/access_log-????????.gz /var/log/old/*/elb-origin-access_log* /var/log/old/*/*/elb-origin-access_log* /var/log/old/*/access_log* /var/log/old/*/*/access_log*` + +and all log entries that contain `HealthChecker` are eliminated as they are only a "ping" request sent by the load balancer to check the instance's health status which is not to be counted as a "hit." Note that according to the above explanations of our log file formats and variants this focuses on hit analysis for those events with broken or partial / incomplete log files such as Kieler Woche 2015 and Travemünder Woche 2015. It uses the `elb-origin-*` flavors, tolerating the fact that these don't have the original client's IP address. + +The third element of AWStats configuration is how it is published through Apache. This is described in `/etc/httpd/conf.d/awstats.conf` and uses the password definition file `/etc/httpd/conf/passwd.awstats` which can be updated using the `htpasswd` command. ### Custom Scripts - - unique_ips_per_referrer - - convertELBLogToApacheFormat \ No newline at end of file +#### `unique_ips_per_referrer` + +The script is located in git at `configuration/unique_ips_per_referrer`. It has two variants next to it: `unique_ips_per_referrer_generate_results_only` and `unique_ips_per_referrer_generate_month_results_only`. The basic idea of this family of scripts is to count the unique visitors for each virtual host name (usually corresponding to an event), in total and by month. Neither the `goaccess` nor the AWStats tool provide us with these numbers. AWStats can only compute the _hits_ per virtual host name, and `goaccess` only lists unique visitors per day, not per virtual host. + +The script can be fed with a list of log files in our common Apache format, with virtual host name in first, original client IP in second, time stamp in third and user agent in last column. For all new files (files already analyzed are recorded in `/var/log/old/cache/unique-ips-per-referrer/visited) the script then reduces each line to its timestamp, original client IP and user agent fields, constituting the key for identifying a unique visitor. Each log entry that has been analyzed and reduced this way is then appended to a file under `/var/log/old/cache/unique-ips-per-referrer/stats/.ips`. When done with all inputs, the `*.ips` files produced are filtered for unique entries (using `sort -u`) and number of lines are counted, resulting in the total number of unique visitors per virtual host name since the beginning of our log file records. + +Additionally, a per-month analysis is carried out by splitting the `*.ips` files by the months found in the time stamps and again applying a unique count. The resulting files can be found at `/var/log/old/cache/unique-ips-per-referrer/stats/unique-ips-days-useragents-per-event` and `/var/log/old/cache/unique-ips-per-referrer/stats/unique-ips-days-useragents-per-event-MMM-YYYY` where `MMM` represents the three-letter month name such as `Apr` and `YYYY` stands for the year. + +The `unique_ips_per_referrer` script is registered as a weekly cron job in `/etc/cron.weekly` and is fed the special converted log files of Travemünder Woche 2015 and the Bundesliga logs from 2015, as well as all files matching the file name pattern `access_log-*` anywhere under `/var/log/old`. This only matches proper Apache log files with original client IP addresses, not those with ELB IP addresses only (which are named `elb-origin-access_log*` by convention, see above). + +The variant `unique_ips_per_referrer_generate_results_only` assumes that the `*.ips` files have already been updated and produces the total and per-month output files. + +The variant `unique_ips_per_referrer_generate_month_results_only` produces only the per-month output files, also assuming that the `*.ips` files have already been updated. + +The results are published under the link [http://awstats.sapsailing.com/unique-visitors/](http://awstats.sapsailing.com/unique-visitors/) which uses the same credentials as the main AWStats publishing page. + +#### `convertELBLogToApacheFormat` + +This script was already briefly mentioned above. It is found at `configuration/convertELBLogToApacheFormat` and converts and Amazon Elastic Load Balancer (ELB) log file to our common Apache log file format with leading virtual host name. The virtual host name is expected as the first parameter; all subsequent parameters are treated as file names of ELB log files, either in GZIP format with `.gz` extension or uncompressed. The conversion result is streamed to the standard output and may therefore be redirected into a file. \ No newline at end of file diff --git a/wiki/creating-ec2-image-from-scratch.md b/wiki/creating-ec2-image-from-scratch.md index 65a5ae35d1d..b9f5b4bae28 100644 --- a/wiki/creating-ec2-image-from-scratch.md +++ b/wiki/creating-ec2-image-from-scratch.md @@ -19,7 +19,8 @@ I then did a `yum update` and added the following packages: - goaccess - postfix (for sending e-mail, e.g., to invite competitors and buoy pingers) - tigervnc-server - - xdm (to have a rudimentary window manager available for the VNC server) + - WindowMaker + - xterm Then I created a mount point /home/sailing and copied the following lines from the /etc/fstab file from an existing SL instance: diff --git a/wiki/onboarding.md b/wiki/onboarding.md index 12d24ae467a..d4a53baa226 100644 --- a/wiki/onboarding.md +++ b/wiki/onboarding.md @@ -67,11 +67,13 @@ First of all, make sure you've looked at http://www.amazon.de/Patterns-Elements- * Rebuild all projects 4. Run the Race Analysis Suite * Start the MongoDB - * Start the appropriate Eclipse launch configuration (e.g. 'Sailing Server (Proxy)') You´ll find this in the run dropdown - * Run "Security UI sdm" in the run dropdown - * Run "SailingGWT" in the run dropdown -5. Within the Race Analysis Suite + * Start the appropriate Eclipse launch configuration (e.g. 'Sailing Server (Proxy)') You´ll find this in the debug dropdown + * Run "Security UI sdm" in the debug dropdown + * Run "SailingGWT" in the debug dropdown +5. Import races within the Race Analysis Suite + * Choose "Security UI sdm" in the upper left corner of the "Development Mode" Tab in Eclipse and open "...Login.html" in your browser * Default Login: user "admin", password "admin" + * Choose "Sailing GWT" in the "Development Mode" Tab and open "...AdminConsole.html..." (It is normal that the first try fails. Reload the page after the first try) * For TracTrac Events: (Date 27.11.2012) Use Live URI tcp://10.18.22.156:4412, Stored URI tcp://10.18.22.156:4413, JSON URL http://germanmaster.traclive.dk/events/event_20120905_erEuropean/jsonservice.php * Press List Races From 106924dc0d9a8a41897163cccd9ebebffdfee7b4 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 7 Dec 2015 17:07:54 +0100 Subject: [PATCH 115/306] newSelectionModel: RefreshableSelectionModel now have a ListDataProvider --- .../celltable/RefreshableMultiSelectionModel.java | 14 ++++++++++---- .../celltable/RefreshableSingleSelectionModel.java | 14 ++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 4f5ba6bc453..c002e33621f 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -1,5 +1,6 @@ package com.sap.sse.gwt.client.celltable; +import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -30,14 +31,17 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; private boolean dontcheckSelectionState = false; + private final ListDataProvider listDataProvider; /** * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects */ - public RefreshableMultiSelectionModel(EntityIdentityComparator comp) { + public RefreshableMultiSelectionModel(EntityIdentityComparator comp, ListDataProvider listDataProvider) { super(); this.comp = comp; + this.listDataProvider = listDataProvider; + this.listDataProvider.addDataDisplay(this); } /** @@ -46,9 +50,11 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects */ - public RefreshableMultiSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { + public RefreshableMultiSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp, ListDataProvider listDataProvider) { super(keyProvider); this.comp = comp; + this.listDataProvider = listDataProvider; + this.listDataProvider.addDataDisplay(this); } @Override @@ -115,8 +121,8 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel } @Override - public void setRowData(int start, List values) { - //TODO /FIME refresh selectionModel with all data of ListDataProvider + public void setRowData(int start, List values) { + refreshSelectionModel(new ArrayList<>(listDataProvider.getList())); } @Override diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index 0927f3145de..d0b5eb07f6b 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -1,8 +1,10 @@ package com.sap.sse.gwt.client.celltable; +import java.util.ArrayList; import java.util.List; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.view.client.HasData; +import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.ProvidesKey; import com.google.gwt.view.client.Range; import com.google.gwt.view.client.SelectionChangeEvent; @@ -19,14 +21,17 @@ import com.google.gwt.view.client.RangeChangeEvent.Handler; public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; private boolean dontcheckSelectionState = false; + private final ListDataProvider listDataProvider; /** * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects */ - public RefreshableSingleSelectionModel(EntityIdentityComparator comp) { + public RefreshableSingleSelectionModel(EntityIdentityComparator comp, ListDataProvider listDataProvider) { super(); this.comp=comp; + this.listDataProvider = listDataProvider; + this.listDataProvider.addDataDisplay(this); } /** @@ -35,9 +40,11 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects */ - public RefreshableSingleSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp) { + public RefreshableSingleSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp, ListDataProvider listDataProvider) { super(keyProvider); this.comp =comp; + this.listDataProvider = listDataProvider; + this.listDataProvider.addDataDisplay(this); } @Override @@ -79,8 +86,7 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel @Override public void setRowData(int start, List values) { - //TODO / FIXME update with all elements of ListDataProvider - + refreshSelectionModel(new ArrayList<>(listDataProvider.getList())); } @Override From abed2beb9718ca9c47a6a93e7328a715a26c4ee5 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 7 Dec 2015 17:42:27 +0100 Subject: [PATCH 116/306] added a release notes entry for bug 3344 --- .../gwt/home/solutions/SailingAnalyticsNotes.html | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html index e5133b4ddbb..69c32d47aea 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html @@ -8,11 +8,11 @@
    • New line hover behavior in race map. When the mouse is moved onto a line, for example the start line, a transparent, - wider line is activated behind it that acts as a buffer for the thin line, where the text displayed by the hover is still shown. - This makes it easier to hover over the lines. These wider lines can be made semi transparent instead of transparent - by unticking the corresponding checkbox in the settings of the race map. Furthermore the stroke weight of the wider lines can be - configured in the same settings dialog. -
    • + wider line is activated behind it that acts as a buffer for the thin line, where the text displayed by the hover is still shown. + This makes it easier to hover over the lines. These wider lines can be made semi transparent instead of transparent + by unticking the corresponding checkbox in the settings of the race map. Furthermore the stroke weight of the wider lines can be + configured in the same settings dialog. +
    • The race viewer (RaceBoard.html) now intelligently chooses a layout that suits the screen size. When only little horizontal space is available, only the sail number and nationality but not the competitor's long name is displayed by default. When vertical space is scarce, competitor and wind @@ -22,6 +22,11 @@
    • With the changes described above, the RaceBoard is now also accessible from the races page on mobile devices.
    • +
    • For overall / series leaderboards such as a national sailing league or the Extreme Sailing Series, the + live status of the overall leaderboard is now indicated. As long as a live race is ongoing, the respective + column in the overall leaderboard is highlighted, and a comment is displayed that indicates which regatta + is currently still live. +
    November 2015
    From 61d8587445b12e1fe2f1acac2366f87f588a3463 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Mon, 7 Dec 2015 18:02:37 +0100 Subject: [PATCH 117/306] newSelectionModel: some refactoring to make the RefreshableSelectionModels a listener of ListDataProvider --- .../AbstractTrackedRacesListComposite.java | 2 +- .../DeviceConfigurationListComposite.java | 13 ++++---- .../DeviceConfigurationPanel.java | 16 ++++------ .../DeviceConfigurationUserListComposite.java | 6 ++-- .../DeviceConfigurationUserPanel.java | 5 ++-- .../ui/adminconsole/IgtimiAccountsPanel.java | 4 +-- .../LeaderboardGroupConfigPanel.java | 2 +- .../gwt/ui/adminconsole/MediaPanel.java | 2 +- .../RemoteServerInstancesManagementPanel.java | 5 ++-- .../SwissTimingEventManagementPanel.java | 5 ++-- .../SwissTimingReplayConnectorPanel.java | 5 ++-- .../client/component/UserManagementPanel.java | 30 +++++++------------ 12 files changed, 41 insertions(+), 54 deletions(-) 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 5ec00888c20..d1e51d56445 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 @@ -141,7 +141,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl refreshableSelectionModel = selectionCheckboxColumn.getSelectionModel(); raceTable.setSelectionModel(refreshableSelectionModel, this.selectionCheckboxColumn.getSelectionManager()); } else { - refreshableSelectionModel = new RefreshableSingleSelectionModel(entityIdentityComparator); + refreshableSelectionModel = new RefreshableSingleSelectionModel(entityIdentityComparator, raceList); raceTable.setSelectionModel(refreshableSelectionModel); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java index e786dde412b..ead317fa410 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationListComposite.java @@ -18,13 +18,13 @@ import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; +import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; public class DeviceConfigurationListComposite extends Composite { protected static AdminConsoleTableResources tableResource = GWT.create(AdminConsoleTableResources.class); - private final RefreshableSelectionModel refreshableConfigurationSelectionModel; + private final RefreshableMultiSelectionModel refreshableConfigurationSelectionModel; private final CellTable configurationTable; protected ListDataProvider configurationsDataProvider; @@ -38,7 +38,6 @@ public class DeviceConfigurationListComposite extends Composite { protected final StringMessages stringMessages; public DeviceConfigurationListComposite(final SailingServiceAsync sailingService, - final RefreshableSelectionModel refreshableSelectionModel, final ErrorReporter errorReporter, final StringMessages stringMessages) { this.sailingService = sailingService; this.errorReporter = errorReporter; @@ -57,8 +56,8 @@ public class DeviceConfigurationListComposite extends Composite { configurationTable = createConfigurationTable(); configurationTable.setVisible(true); - - refreshableConfigurationSelectionModel = refreshableSelectionModel; + // TODO / FIXME Lukas define EntityIdentityComparator + refreshableConfigurationSelectionModel = new RefreshableMultiSelectionModel<>(null, configurationsDataProvider); configurationTable.setSelectionModel(refreshableConfigurationSelectionModel); panel.add(configurationTable); @@ -136,4 +135,8 @@ public class DeviceConfigurationListComposite extends Composite { table.addColumn(identifierNameColumn, stringMessages.device()); return table; } + + public RefreshableMultiSelectionModel getSelectionModel() { + return refreshableConfigurationSelectionModel; + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java index 9ac8791a72a..ff483265f84 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationPanel.java @@ -22,7 +22,6 @@ import com.sap.sailing.gwt.ui.shared.DeviceConfigurationDTO; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; -import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; @@ -55,9 +54,8 @@ public class DeviceConfigurationPanel extends SimplePanel implements DeviceConfi this.sailingService = sailingService; this.stringMessages = stringMessages; this.errorReporter = reporter; - - this.refreshableMultiSelectionModel = new RefreshableMultiSelectionModel<>(null); - //TODO create an appropriate EntityIdentityComparator or define an .eqauls() method for DeviceConfigurationMatcherDTO + setupUi(); + refreshableMultiSelectionModel = listComposite.getSelectionModel(); refreshableMultiSelectionModel.addSelectionChangeHandler(new Handler() { @Override @@ -73,8 +71,7 @@ public class DeviceConfigurationPanel extends SimplePanel implements DeviceConfi } removeConfigurationButton.setEnabled(!selectedConfigurations.isEmpty()); } - }); - setupUi(); + }); } private void setupUi() { @@ -124,7 +121,7 @@ public class DeviceConfigurationPanel extends SimplePanel implements DeviceConfi Grid grid = new Grid(1 ,2); mainPanel.add(grid); - listComposite = createListComposite(sailingService, refreshableMultiSelectionModel, errorReporter, stringMessages); + listComposite = createListComposite(sailingService, errorReporter, stringMessages); grid.setWidget(0, 0, listComposite); grid.getRowFormatter().setVerticalAlign(0, HasVerticalAlignment.ALIGN_TOP); grid.getColumnFormatter().getElement(1).getStyle().setPaddingTop(2.0, Unit.EM); @@ -185,10 +182,9 @@ public class DeviceConfigurationPanel extends SimplePanel implements DeviceConfi return new DeviceConfigurationCreateMatcherDialog(stringMessages, validator, callback); } - protected DeviceConfigurationListComposite createListComposite(SailingServiceAsync sailingService, RefreshableSelectionModel selectionModel, + protected DeviceConfigurationListComposite createListComposite(SailingServiceAsync sailingService, ErrorReporter errorReporter, StringMessages stringMessages) { - return new DeviceConfigurationListComposite(sailingService, selectionModel, - errorReporter, stringMessages); + return new DeviceConfigurationListComposite(sailingService, errorReporter, stringMessages); } protected DeviceConfigurationDetailComposite createDetailComposite(SailingServiceAsync sailingService, diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserListComposite.java index afbe5e87279..3e38ef265d2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserListComposite.java @@ -9,14 +9,12 @@ import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; public class DeviceConfigurationUserListComposite extends DeviceConfigurationListComposite { - public DeviceConfigurationUserListComposite(SailingServiceAsync sailingService, - RefreshableSelectionModel refreshableSelectionModel, ErrorReporter errorReporter, + public DeviceConfigurationUserListComposite(SailingServiceAsync sailingService, ErrorReporter errorReporter, StringMessages stringMessages) { - super(sailingService, refreshableSelectionModel, errorReporter, stringMessages); + super(sailingService, errorReporter, stringMessages); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserPanel.java index ebf1f53ca3d..38eed18e701 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/DeviceConfigurationUserPanel.java @@ -5,7 +5,6 @@ import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.DeviceConfigurationMatcherDTO; import com.sap.sse.gwt.client.ErrorReporter; -import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; @@ -18,9 +17,9 @@ public class DeviceConfigurationUserPanel extends DeviceConfigurationPanel { } @Override - protected DeviceConfigurationListComposite createListComposite(SailingServiceAsync sailingService, RefreshableSelectionModel refreshableSelectionModel, + protected DeviceConfigurationListComposite createListComposite(SailingServiceAsync sailingService, ErrorReporter errorReporter, StringMessages stringMessages) { - return new DeviceConfigurationUserListComposite(sailingService, refreshableSelectionModel, errorReporter, stringMessages); + return new DeviceConfigurationUserListComposite(sailingService, errorReporter, stringMessages); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java index f1080c42761..e2b7decab9e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java @@ -76,9 +76,9 @@ public class IgtimiAccountsPanel extends FlowPanel { AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); allAccounts = new CellTable(/* pageSize */10000, tableRes); - refreshableAccountsSelectionModel = new RefreshableSingleSelectionModel(null); - allAccounts.setSelectionModel(refreshableAccountsSelectionModel); final ListDataProvider filteredAccounts = new ListDataProvider(); + refreshableAccountsSelectionModel = new RefreshableSingleSelectionModel(null, filteredAccounts); + allAccounts.setSelectionModel(refreshableAccountsSelectionModel); ListHandler accountColumnListHandler = new ListHandler(filteredAccounts.getList()); filteredAccounts.addDataDisplay(allAccounts); final List emptyList = Collections.emptyList(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java index 3ec43e1a9f8..b1b72cd85d4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java @@ -580,7 +580,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements public boolean representSameEntity(LeaderboardGroupDTO dto1, LeaderboardGroupDTO dto2) { return dto1.getId().equals(dto2.getId()); } - }); + }, groupsProvider); refreshableGroupsSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java index 61507265892..a51141a9f62 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java @@ -171,7 +171,7 @@ public class MediaPanel extends FlowPanel { public boolean representSameEntity(MediaTrack dto1, MediaTrack dto2) { return dto1.dbId.equals(dto2.dbId); } - }); + }, mediaTrackListDataProvider); mediaTracksTable.setSelectionModel(refreshableSelectionModel, DefaultSelectionEventManager. createDefaultManager()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java index 3b915c4afd6..d3f5ccf03e2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RemoteServerInstancesManagementPanel.java @@ -41,6 +41,7 @@ public class RemoteServerInstancesManagementPanel extends SimplePanel { private final StringMessages stringMessages; private final AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); + private final ListDataProvider serverDataProvider; private RefreshableMultiSelectionModel refreshableServerSelectionModel; private LabeledAbstractFilterablePanel filteredServerTablePanel; @@ -61,8 +62,8 @@ public class RemoteServerInstancesManagementPanel extends SimplePanel { VerticalPanel remoteServersContentPanel = new VerticalPanel(); remoteServersPanel.setContentWidget(remoteServersContentPanel); + serverDataProvider = new ListDataProvider(); CellTable remoteServersTable = createRemoteServersTable(); - ListDataProvider serverDataProvider = new ListDataProvider(); serverDataProvider.addDataDisplay(remoteServersTable); filteredServerTablePanel = new LabeledAbstractFilterablePanel( @@ -167,7 +168,7 @@ public class RemoteServerInstancesManagementPanel extends SimplePanel { RemoteSailingServerReferenceDTO dto2) { return dto1.getUrl().equals(dto2.getUrl()); } - }); + }, serverDataProvider); serverTable.setSelectionModel(refreshableServerSelectionModel); return serverTable; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java index 52362ff5af1..28f808a77eb 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java @@ -262,6 +262,7 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane raceTable.addColumn(genderColumn, "Gender"); raceTable.addColumn(raceStartTimeColumn, stringConstants.startTime()); raceTable.setWidth("300px"); + raceList = new ListDataProvider(); raceTable.setSelectionModel(new RefreshableMultiSelectionModel( new EntityIdentityComparator() { @@ -269,12 +270,10 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane public boolean representSameEntity(SwissTimingRaceRecordDTO dto1, SwissTimingRaceRecordDTO dto2) { return dto1.raceId.equals(dto2.raceId); } - }) { + }, raceList) { }); trackableRacesPanel.add(raceTable); - - raceList = new ListDataProvider(); raceList.addDataDisplay(raceTable); Handler columnSortHandler = getRaceTableColumnSortHandler(raceList.getList(), regattaNameColumn, seriesNameColumn, raceNameColumn, raceStartTimeColumn, raceIdColumn, boatClassColumn, genderColumn, raceStatusColumn); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java index 53e64474acd..6893c96486d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java @@ -190,6 +190,7 @@ public class SwissTimingReplayConnectorPanel extends AbstractEventManagementPane raceTable.addColumn(boatClassNamesColumn, stringMessages.boatClass()); raceTable.addColumn(raceStartTrackingColumn, stringMessages.startTime()); raceTable.setWidth("300px"); + raceList = new ListDataProvider(); raceTable.setSelectionModel(new RefreshableMultiSelectionModel( new EntityIdentityComparator() { @@ -197,13 +198,11 @@ public class SwissTimingReplayConnectorPanel extends AbstractEventManagementPane public boolean representSameEntity(SwissTimingReplayRaceDTO dto1, SwissTimingReplayRaceDTO dto2) { return dto1.race_id.equals(dto2.race_id); } - }) { + }, raceList) { }); racesHorizontalPanel.add(raceTable); racesHorizontalPanel.add(trackPanel); - - raceList = new ListDataProvider(); raceList.addDataDisplay(raceTable); Handler columnSortHandler = getRaceTableColumnSortHandler(raceList.getList(), raceNameColumn, boatClassNamesColumn, raceStartTrackingColumn); raceTable.addColumnSortHandler(columnSortHandler); diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java index 022759c56ac..3e44dd457ae 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserManagementPanel.java @@ -22,8 +22,7 @@ import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SelectionChangeEvent.Handler; -import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; -import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; +import com.google.gwt.view.client.SingleSelectionModel; import com.sap.sse.security.shared.PermissionsForRoleProvider; import com.sap.sse.security.ui.client.UserChangeEventHandler; import com.sap.sse.security.ui.client.UserManagementServiceAsync; @@ -38,7 +37,7 @@ public class UserManagementPanel extends DockPanel { private List userDeletedHandlers = new ArrayList<>(); - private RefreshableSingleSelectionModel refreshableSingleSelectionModel; + private SingleSelectionModel singleSelectionModel; private UserListDataProvider userListDataProvider; @@ -47,14 +46,7 @@ public class UserManagementPanel extends DockPanel { VerticalPanel west = new VerticalPanel(); HorizontalPanel buttonPanel = new HorizontalPanel(); west.add(buttonPanel); - refreshableSingleSelectionModel = new RefreshableSingleSelectionModel<>( - new EntityIdentityComparator() { - - @Override - public boolean representSameEntity(UserDTO dto1, UserDTO dto2) { - return dto1.getEmail().equals(dto2.getEmail()); - } - }); + singleSelectionModel = new SingleSelectionModel<>(); Button createButton = new Button(stringMessages.createUser(), new ClickHandler() { @Override public void onClick(ClickEvent event) { @@ -65,7 +57,7 @@ public class UserManagementPanel extends DockPanel { final Button deleteButton = new Button(stringMessages.deleteUser(), new ClickHandler() { @Override public void onClick(ClickEvent event) { - final UserDTO userToDelete = refreshableSingleSelectionModel.getSelectedObject(); + final UserDTO userToDelete = singleSelectionModel.getSelectedObject(); final String username = userToDelete.getName(); if (Window.confirm(stringMessages.doYouReallyWantToDeleteUser(username))) { userManagementService.deleteUser(username, new AsyncCallback() { @@ -86,12 +78,12 @@ public class UserManagementPanel extends DockPanel { } }); buttonPanel.add(deleteButton); - deleteButton.setEnabled(refreshableSingleSelectionModel.getSelectedObject() != null); + deleteButton.setEnabled(singleSelectionModel.getSelectedObject() != null); final UserList userList = new UserList(); - userList.setSelectionModel(refreshableSingleSelectionModel); + userList.setSelectionModel(singleSelectionModel); TextBox filterBox = new TextBox(); userListDataProvider = new UserListDataProvider(userManagementService, filterBox); - final UserDetailsView userDetailsView = new UserDetailsView(userService, refreshableSingleSelectionModel.getSelectedObject(), stringMessages, userListDataProvider, permissionsForRoleProvider); + final UserDetailsView userDetailsView = new UserDetailsView(userService, singleSelectionModel.getSelectedObject(), stringMessages, userListDataProvider, permissionsForRoleProvider); add(userDetailsView, DockPanel.CENTER); userDetailsView.addUserChangeEventHandler(new UserChangeEventHandler() { @Override @@ -99,11 +91,11 @@ public class UserManagementPanel extends DockPanel { userListDataProvider.updateDisplays(); } }); - refreshableSingleSelectionModel.addSelectionChangeHandler(new Handler() { + singleSelectionModel.addSelectionChangeHandler(new Handler() { @Override public void onSelectionChange(SelectionChangeEvent event) { - userDetailsView.updateUser(refreshableSingleSelectionModel.getSelectedObject(), userManagementService); - deleteButton.setEnabled(refreshableSingleSelectionModel.getSelectedObject() != null); + userDetailsView.updateUser(singleSelectionModel.getSelectedObject(), userManagementService); + deleteButton.setEnabled(singleSelectionModel.getSelectedObject() != null); } }); userList.setPageSize(20); @@ -135,7 +127,7 @@ public class UserManagementPanel extends DockPanel { if (user != null) { for (UserDTO u : userList.getVisibleItems()) { if (u.getName().equals(user.getName())) { - refreshableSingleSelectionModel.setSelected(user, true); + singleSelectionModel.setSelected(user, true); } } } From fe3bfbe9f69c2c3d015f8e8229db261bffb1228f Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Mon, 7 Dec 2015 23:19:33 +0100 Subject: [PATCH 118/306] Dashboard uses now dispatch logic from ui bundle to comunicate with server --- .../gwt/client/DashboardClientFactory.java | 2 + .../client/DashboardClientFactoryImpl.java | 20 +- .../gwt/client/RibDashboardEntryPoint.java | 19 +- .../gwt/client/RibDashboardPanel.java | 41 +-- .../GetCompetitorInLeaderboardAction.java | 35 ++ .../GetIDFromRaceThatIsLiveAction.java | 38 +- ...IDFromRaceThatTakesWindFixesNowAction.java | 43 ++- .../actions/GetStartAnalysesAction.java | 68 ++++ ...GetStartlineAdvantageByGeometryAction.java | 48 +++ .../GetStartlineAdvantagesByWindAction.java | 45 ++- .../dataretriever/WindBotDataRetriever.java | 58 +-- .../StartlineAnalysisComponent.java | 110 +++--- ...StartLineAdvantageByGeometryComponent.java | 60 +++- .../StartlineAdvantagesByWindComponent.java | 65 ++-- .../DashboardDispatchRPCServletImpl.java | 24 +- .../gwt/server/LiveTrackedRaceListener.java | 15 - .../gwt/server/LiveTrackedRaceProvider.java | 29 -- .../gwt/server/RibDashboardServiceImpl.java | 331 ------------------ ...bstractStartAnalysisCreationValidator.java | 12 +- .../StartAnalysisCreationController.java | 19 +- .../StartAnalysisDTOFactory.java | 42 ++- .../DefaultPolarValues.java | 2 +- ...efaultPolarWindAngleBoatSpeedFunction.java | 2 +- ...tartlineAdvantageByGeometryCalculator.java | 77 ++++ .../StartlineAdvantagesByWindCalculator.java} | 65 +--- .../AbstracPreCalculationDataRetriever.java | 7 +- .../PreCalculationMarkRetriever.java | 2 +- .../PreCalculationPolarDataRetriever.java | 10 +- ...alculationStartlineAdvantageRetriever.java | 2 +- .../PreCalculationWindRetriever.java | 2 +- .../gwt/shared/DashboardLiveRaceProvider.java | 55 +++ .../dashboards/gwt/shared/MovingAverage.java | 6 +- .../gwt/shared/MovingAveragesCache.java | 44 +++ .../gwt/shared/dispatch/DashboardAction.java | 2 +- .../dispatch/DashboardDispatchContext.java | 12 + .../dispatch/RequiresLiveRaceAction.java | 30 ++ ...sLiveRaceAndCachesMovingAverageAction.java | 30 ++ .../impl/DashboardDispatchContextImpl.java | 30 +- .../dashboards/gwt/shared/dto/AverageDTO.java | 8 +- .../shared/dto/LeaderboardCompetitorsDTO.java | 25 ++ .../gwt/shared/{ => dto}/RaceIdDTO.java | 5 +- .../gwt/shared/dto/StartAnalysesDTO.java | 25 ++ .../gwt/shared/dto/StartLineAdvantageDTO.java | 10 +- ...artlineAdvantagesWithMaxAndAverageDTO.java | 6 +- .../dashboards/gwt/RibDashboard.gwt.xml | 4 +- .../com/sap/sailing/gwt/ui/NoUI.gwt.xml | 6 +- 46 files changed, 866 insertions(+), 725 deletions(-) create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetCompetitorInLeaderboardAction.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartAnalysesAction.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantageByGeometryAction.java delete mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/LiveTrackedRaceListener.java delete mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/LiveTrackedRaceProvider.java delete mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/RibDashboardServiceImpl.java rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/{ => util/actions}/startanalysis/AbstractStartAnalysisCreationValidator.java (79%) rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/{ => util/actions}/startanalysis/StartAnalysisCreationController.java (57%) rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/{ => util/actions}/startanalysis/StartAnalysisDTOFactory.java (79%) rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/{startlineadvantages => util/actions/startlineadvantage}/DefaultPolarValues.java (87%) rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/{startlineadvantages => util/actions/startlineadvantage}/DefaultPolarWindAngleBoatSpeedFunction.java (96%) create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/StartlineAdvantageByGeometryCalculator.java rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/{startlineadvantages/StartlineAdvantagesCalculator.java => util/actions/startlineadvantage/StartlineAdvantagesByWindCalculator.java} (87%) rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/{startlineadvantages => util/actions/startlineadvantage}/precalculation/AbstracPreCalculationDataRetriever.java (86%) rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/{startlineadvantages => util/actions/startlineadvantage}/precalculation/PreCalculationMarkRetriever.java (97%) rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/{startlineadvantages => util/actions/startlineadvantage}/precalculation/PreCalculationPolarDataRetriever.java (73%) rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/{startlineadvantages => util/actions/startlineadvantage}/precalculation/PreCalculationStartlineAdvantageRetriever.java (90%) rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/{startlineadvantages => util/actions/startlineadvantage}/precalculation/PreCalculationWindRetriever.java (84%) create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardLiveRaceProvider.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/MovingAveragesCache.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/RequiresLiveRaceAction.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/RequiresLiveRaceAndCachesMovingAverageAction.java create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/LeaderboardCompetitorsDTO.java rename java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/{ => dto}/RaceIdDTO.java (84%) create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartAnalysesDTO.java diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java index f516121acc0..d667d6bb2ec 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java @@ -1,6 +1,7 @@ package com.sap.sailing.dashboards.gwt.client; import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchSystem; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; /** * @author Alexander Ries (D062114) @@ -8,4 +9,5 @@ import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchSystem; */ public interface DashboardClientFactory{ DashboardDispatchSystem getDispatch(); + SailingServiceAsync getSailingService(); } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java index b704185a2fb..afaacf3799b 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java @@ -1,7 +1,12 @@ package com.sap.sailing.dashboards.gwt.client; +import com.google.gwt.core.client.GWT; +import com.google.gwt.user.client.rpc.ServiceDefTarget; import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchSystem; import com.sap.sailing.dashboards.gwt.shared.dispatch.impl.DashboardDispatchSystemImpl; +import com.sap.sailing.gwt.ui.client.SailingService; +import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sse.gwt.client.EntryPointHelper; /** * @author Alexander Ries (D062114) @@ -9,11 +14,24 @@ import com.sap.sailing.dashboards.gwt.shared.dispatch.impl.DashboardDispatchSyst */ public class DashboardClientFactoryImpl implements DashboardClientFactory{ - private final DashboardDispatchSystem dispatch = new DashboardDispatchSystemImpl(); + private final DashboardDispatchSystem dispatch; + private final SailingServiceAsync sailingService; + + + public DashboardClientFactoryImpl() { + dispatch = new DashboardDispatchSystemImpl(); + sailingService = GWT.create(SailingService.class); + EntryPointHelper.registerASyncService((ServiceDefTarget) sailingService, RemoteServiceMappingConstants.WEB_CONTEXT_PATH, RemoteServiceMappingConstants.sailingServiceRemotePath); + } @Override public DashboardDispatchSystem getDispatch() { return dispatch; } + @Override + public SailingServiceAsync getSailingService() { + return sailingService; + } + } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java index 4cb66669148..cb19f17e03c 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java @@ -1,20 +1,14 @@ package com.sap.sailing.dashboards.gwt.client; -import com.google.gwt.core.client.GWT; -import com.google.gwt.user.client.rpc.ServiceDefTarget; import com.google.gwt.user.client.ui.RootLayoutPanel; import com.googlecode.mgwt.ui.client.MGWT; import com.googlecode.mgwt.ui.client.MGWTSettings; import com.googlecode.mgwt.ui.client.MGWTSettings.StatusBarStyle; import com.googlecode.mgwt.ui.client.MGWTSettings.ViewPort; import com.googlecode.mgwt.ui.client.MGWTStyle; -import com.sap.sailing.dashboards.gwt.client.dataretriever.RibDashboardDataRetriever; import com.sap.sailing.dashboards.gwt.client.dataretriever.WindBotDataRetriever; -import com.sap.sailing.gwt.ui.client.SailingService; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.gwt.client.AbstractEntryPoint; -import com.sap.sse.gwt.client.EntryPointHelper; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.player.Timer.PlayModes; @@ -29,25 +23,16 @@ public class RibDashboardEntryPoint extends AbstractEntryPoint { @Override public void doOnModuleLoad() { - - SailingServiceAsync sailingService = GWT.create(SailingService.class); - EntryPointHelper.registerASyncService((ServiceDefTarget) sailingService, RemoteServiceMappingConstants.WEB_CONTEXT_PATH, RemoteServiceMappingConstants.sailingServiceRemotePath); - - RibDashboardServiceAsync ribdashboardService = GWT.create(RibDashboardService.class); - EntryPointHelper.registerASyncService((ServiceDefTarget) ribdashboardService, RemoteServiceMappingConstants.WEB_CONTEXT_PATH, RemoteServiceMappingConstants.ribdashboardServiceRemotePath); - - RibDashboardDataRetriever dataRetriever = new RibDashboardDataRetriever(ribdashboardService); - applyMGWTSettings(); DashboardClientFactory dashboardClientFactory = new DashboardClientFactoryImpl(); RibDashboardPanel ribDashboardPanel = new RibDashboardPanel(dashboardClientFactory); RootLayoutPanel.get().add(ribDashboardPanel); - /*WindBotDataRetriever windBotDataRetriever = new WindBotDataRetriever(ribdashboardService, sailingService); + WindBotDataRetriever windBotDataRetriever = new WindBotDataRetriever(dashboardClientFactory); windBotDataRetriever.addNumberOfWindBotsChangeListeners(ribDashboardPanel); Timer dashboardTimer = new Timer(PlayModes.Live); dashboardTimer.setRefreshInterval(DASHBOARD_RERFRESH_INTERVAL); dashboardTimer.addTimeListener(windBotDataRetriever); - dashboardTimer.addTimeListener(dataRetriever);*/ +// dashboardTimer.addTimeListener(dataRetriever); } private void applyMGWTSettings() { diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java index 89eb096aa77..7bf66d809f8 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java @@ -20,7 +20,6 @@ import com.google.gwt.user.client.ui.Widget; import com.googlecode.mgwt.ui.client.widget.carousel.Carousel; import com.sap.sailing.dashboards.gwt.client.actions.GetIDFromRaceThatIsLiveAction; import com.sap.sailing.dashboards.gwt.client.dataretriever.NumberOfWindBotsChangeListener; -import com.sap.sailing.dashboards.gwt.client.dataretriever.RibDashboardDataRetriever; import com.sap.sailing.dashboards.gwt.client.dataretriever.WindBotDataRetrieverProvider; import com.sap.sailing.dashboards.gwt.client.eventlogo.EventLogo; import com.sap.sailing.dashboards.gwt.client.notifications.WrongDeviceOrientationNotification; @@ -29,8 +28,7 @@ import com.sap.sailing.dashboards.gwt.client.startlineadvantage.StartLineAdvanta import com.sap.sailing.dashboards.gwt.client.startlineadvantage.StartlineAdvantagesByWindComponent; import com.sap.sailing.dashboards.gwt.client.windchart.WindBotComponent; import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; -import com.sap.sailing.dashboards.gwt.shared.RaceIdDTO; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.dashboards.gwt.shared.dto.RaceIdDTO; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.gwt.client.player.TimeListener; @@ -92,25 +90,23 @@ public class RibDashboardPanel extends Composite implements NumberOfWindBotsChan private List windBotComponents; private StringMessages stringConstants; - private RibDashboardServiceAsync ribDashboardService; DashboardClientFactory dashboardClientFactory; private static final Logger logger = Logger.getLogger(RibDashboardPanel.class.getName()); public RibDashboardPanel(DashboardClientFactory dashboardClientFactory) { this.dashboardClientFactory = dashboardClientFactory; -// this.ribDashboardService = ribDashboardService; -// windBotComponents = new ArrayList(); -// startanalysisComponent = new StartlineAnalysisComponent(ribDashboardService, sailingServiceAsync); -// startlineAdvantagesByWindComponent = new StartlineAdvantagesByWindComponent(ribDashboardService); -// startlineAdvantageByGeometryComponent = new StartLineAdvantageByGeometryComponent(ribDashboardDataRetriever); + windBotComponents = new ArrayList(); + startanalysisComponent = new StartlineAnalysisComponent(this.dashboardClientFactory); + startlineAdvantagesByWindComponent = new StartlineAdvantagesByWindComponent(this.dashboardClientFactory); + startlineAdvantageByGeometryComponent = new StartLineAdvantageByGeometryComponent(this.dashboardClientFactory); stringConstants = StringMessages.INSTANCE; initWidget(uiBinder.createAndBindUi(this)); -// initLogos(); -// windcharthint.getElement().setInnerText(stringConstants.dashboardWindChartHint()); -// windloadinghintleft.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
    "+stringConstants.dashboardWindBotLoadingMessage()); -// windloadinghintright.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
    "+stringConstants.dashboardWindBotLoadingMessage()); + initLogos(); + windcharthint.getElement().setInnerText(stringConstants.dashboardWindChartHint()); + windloadinghintleft.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
    "+stringConstants.dashboardWindBotLoadingMessage()); + windloadinghintright.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
    "+stringConstants.dashboardWindBotLoadingMessage()); startUpdatingRaceLabelIn20SecondInterval(); -// loadAndAddEventLogo(sailingServiceAsync); + loadAndAddEventLogo(dashboardClientFactory.getSailingService()); initAndAddWrongOrientationNotification(); } @@ -191,19 +187,20 @@ public class RibDashboardPanel extends Composite implements NumberOfWindBotsChan new GetIDFromRaceThatIsLiveAction(leaderboardNameParameterValue), new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - logger.log(Level.INFO, - "Failed to received RegattaAndRaceIdentifier from race that is live, "+caught.getMessage()); - } - @Override public void onSuccess(RaceIdDTO result) { - if (result != null) { - logger.log(Level.INFO, "Updating UI with RibDashboardRaceInfoDTO race name"); + if (result != null && result.getRaceId() != null) { + logger.log(Level.INFO, "Updating UI with live race id"); setHeaderText(result.getRaceId().getRaceName()); + } else { + logger.log(Level.INFO, "Received null for live race id"); } } + + @Override + public void onFailure(Throwable caught) { + logger.log(Level.INFO, "Failed to received live race id"+caught.getMessage()); + } }); } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetCompetitorInLeaderboardAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetCompetitorInLeaderboardAction.java new file mode 100644 index 00000000000..433536bec7e --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetCompetitorInLeaderboardAction.java @@ -0,0 +1,35 @@ +package com.sap.sailing.dashboards.gwt.client.actions; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardAction; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; +import com.sap.sailing.dashboards.gwt.shared.dto.LeaderboardCompetitorsDTO; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +/** + * @author Alexander Ries (D062114) + * + */ +public class GetCompetitorInLeaderboardAction implements DashboardAction { + + private String leaderboarName; + + @SuppressWarnings("unused") + private GetCompetitorInLeaderboardAction() { + } + + public GetCompetitorInLeaderboardAction(String leaderboardName) { + this.leaderboarName = leaderboardName; + } + + @Override + @GwtIncompatible + public LeaderboardCompetitorsDTO execute(DashboardDispatchContext dashboardDispatchContext) throws DispatchException { + LeaderboardCompetitorsDTO result = new LeaderboardCompetitorsDTO(); + Leaderboard lb = dashboardDispatchContext.getRacingEventService().getLeaderboardByName(this.leaderboarName); + result.setCompetitors(dashboardDispatchContext.getRacingEventService().getBaseDomainFactory().getCompetitorDTOList(lb.getCompetitorsFromBestToWorst(MillisecondsTimePoint.now()))); + return result; + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatIsLiveAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatIsLiveAction.java index cd5a3952d54..c26ab8c6d0c 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatIsLiveAction.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatIsLiveAction.java @@ -1,50 +1,32 @@ package com.sap.sailing.dashboards.gwt.client.actions; import com.google.gwt.core.shared.GwtIncompatible; -import com.sap.sailing.dashboards.gwt.shared.RaceIdDTO; -import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardAction; +import com.sap.sailing.dashboards.gwt.shared.dispatch.RequiresLiveRaceAction; import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.dashboards.gwt.shared.dto.RaceIdDTO; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; -import com.sap.sse.common.impl.MillisecondsTimePoint; /** * @author Alexander Ries (D062114) * */ -public class GetIDFromRaceThatIsLiveAction implements DashboardAction { - - private String leaderboarName; - - @SuppressWarnings("unused") - private GetIDFromRaceThatIsLiveAction() { - } +public class GetIDFromRaceThatIsLiveAction extends RequiresLiveRaceAction { + public GetIDFromRaceThatIsLiveAction() {} + public GetIDFromRaceThatIsLiveAction(String leaderboardName) { - this.leaderboarName = leaderboardName; + super(leaderboardName); } @Override @GwtIncompatible - public RaceIdDTO execute(DashboardDispatchContext ctx) throws DispatchException { + public RaceIdDTO execute(DashboardDispatchContext dashboardDispatchContext) throws DispatchException { RaceIdDTO result = new RaceIdDTO(); - TrackedRace liveRace = null; - Leaderboard leaderboard = ctx.getRacingEventService().getLeaderboardByName(this.leaderboarName); - if (leaderboard != null) { - for (RaceColumn column : leaderboard.getRaceColumns()) { - for (Fleet fleet : column.getFleets()) { - TrackedRace race = column.getTrackedRace(fleet); - if (race != null && race.isLive(MillisecondsTimePoint.now())) { - liveRace = race; - } - } - } - } - if (liveRace != null) + TrackedRace liveRace = super.getLiveRace(dashboardDispatchContext); + if (liveRace != null) { result.setRaceId(liveRace.getRaceIdentifier()); + } return result; } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatTakesWindFixesNowAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatTakesWindFixesNowAction.java index 4405378671b..25491706bb6 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatTakesWindFixesNowAction.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetIDFromRaceThatTakesWindFixesNowAction.java @@ -1,26 +1,47 @@ package com.sap.sailing.dashboards.gwt.client.actions; -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sse.gwt.client.async.AsyncAction; +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardAction; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; +import com.sap.sailing.dashboards.gwt.shared.dto.RaceIdDTO; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; +import com.sap.sse.common.impl.MillisecondsTimePoint; /** * @author Alexander Ries (D062114) * */ -public class GetIDFromRaceThatTakesWindFixesNowAction implements AsyncAction { +public class GetIDFromRaceThatTakesWindFixesNowAction implements DashboardAction { - private final RibDashboardServiceAsync ribDashboardService; - private final String leaderboardName; + private String leaderboardName; - public GetIDFromRaceThatTakesWindFixesNowAction(RibDashboardServiceAsync ribDashboardService, String leaderboardName) { - this.ribDashboardService = ribDashboardService; + @SuppressWarnings("unused") + private GetIDFromRaceThatTakesWindFixesNowAction() { + } + + public GetIDFromRaceThatTakesWindFixesNowAction(String leaderboardName) { this.leaderboardName = leaderboardName; } @Override - public void execute(AsyncCallback callback) { - ribDashboardService.getIDFromRaceThatTakesWindFixesNow(leaderboardName, callback); + @GwtIncompatible + public RaceIdDTO execute(DashboardDispatchContext dashboardDispatchContext) throws DispatchException { + RaceIdDTO result = new RaceIdDTO(); + Leaderboard leaderboard = dashboardDispatchContext.getRacingEventService().getLeaderboardByName(this.leaderboardName); + if (leaderboard != null) { + for (RaceColumn column : leaderboard.getRaceColumns()) { + for (Fleet fleet : column.getFleets()) { + TrackedRace race = column.getTrackedRace(fleet); + if (race != null && race.takesWindFixWithTimePoint(MillisecondsTimePoint.now())) { + result.setRaceId(race.getRaceIdentifier()); + } + } + } + } + return result; } } \ No newline at end of file diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartAnalysesAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartAnalysesAction.java new file mode 100644 index 00000000000..e03010ed153 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartAnalysesAction.java @@ -0,0 +1,68 @@ +package com.sap.sailing.dashboards.gwt.client.actions; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.dashboards.gwt.server.util.actions.startanalysis.StartAnalysisCreationController; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardAction; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; +import com.sap.sailing.dashboards.gwt.shared.dto.StartAnalysesDTO; +import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisDTO; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; + +/** + * @author Alexander Ries (D062114) + * + */ +public class GetStartAnalysesAction implements DashboardAction { + + private String leaderboarName; + private String competitorIdAsString; + + @SuppressWarnings("unused") + private GetStartAnalysesAction() { + } + + public GetStartAnalysesAction(String leaderboardName, String competitorIdAsString) { + this.leaderboarName = leaderboardName; + this.competitorIdAsString = competitorIdAsString; + } + + @Override + @GwtIncompatible + public StartAnalysesDTO execute(DashboardDispatchContext dashboardDispatchContext) throws DispatchException { + StartAnalysesDTO result = new StartAnalysesDTO(); + List startanalyses = new ArrayList(); + if (competitorIdAsString != null && leaderboarName != null) { + try { + Competitor competitor = dashboardDispatchContext.getRacingEventService().getBaseDomainFactory().getCompetitorStore().getExistingCompetitorByIdAsString( + competitorIdAsString); + Leaderboard leaderboard = dashboardDispatchContext.getRacingEventService().getLeaderboardByName(this.leaderboarName); + if (leaderboard != null) { + for (RaceColumn column : leaderboard.getRaceColumns()) { + for (Fleet fleet : column.getFleets()) { + TrackedRace race = column.getTrackedRace(fleet); + if (race != null) { + StartAnalysisDTO startAnalysisDTO = StartAnalysisCreationController.checkStartAnalysisForCompetitorInTrackedRace(dashboardDispatchContext, competitor, race); + if (startAnalysisDTO != null) { + startanalyses.add(startAnalysisDTO); + } + } + } + } + + } + } catch (NullPointerException e) { + // logger.log(Level.INFO, "", e); + } + } + result.setStartAnalyses(startanalyses); + return result; + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantageByGeometryAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantageByGeometryAction.java new file mode 100644 index 00000000000..480db85ec67 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantageByGeometryAction.java @@ -0,0 +1,48 @@ +package com.sap.sailing.dashboards.gwt.client.actions; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.dashboards.gwt.client.startanalysis.StartlineAdvantageType; +import com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage.StartlineAdvantageByGeometryCalculator; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; +import com.sap.sailing.dashboards.gwt.shared.dispatch.RequiresLiveRaceAndCachesMovingAverageAction; +import com.sap.sailing.dashboards.gwt.shared.dto.StartLineAdvantageDTO; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; + +/** + * @author Alexander Ries (D062114) + * + */ +public class GetStartlineAdvantageByGeometryAction extends RequiresLiveRaceAndCachesMovingAverageAction { + + public GetStartlineAdvantageByGeometryAction() {} + + public GetStartlineAdvantageByGeometryAction(String leaderboardName) { + super(leaderboardName); + } + + @Override + @GwtIncompatible + public StartLineAdvantageDTO execute(DashboardDispatchContext dashboardDispatchContext) throws DispatchException { + StartLineAdvantageDTO result = new StartLineAdvantageDTO(); + Double startlineAdvantageByGeometry = null; + TrackedRace liveRace = super.getLiveRace(dashboardDispatchContext); + if (liveRace != null) { + startlineAdvantageByGeometry = StartlineAdvantageByGeometryCalculator.calculateStartlineAdvantageByGeometry(liveRace); + } + result.startLineAdvatageType = StartlineAdvantageType.GEOMETRIC; + result.startLineAdvantage = startlineAdvantageByGeometry; + Double average = null; + if (startlineAdvantageByGeometry != null) { + super.addValueToMovingAverage(startlineAdvantageByGeometry.doubleValue(), dashboardDispatchContext.getMovingAveragesCache()); + average = dashboardDispatchContext.getMovingAveragesCache().getValueForKey(getKeyForMovingAverage()); + } + result.average = average; + return result; + } + + @Override + protected String getKeyForMovingAverage() { + return super.getLeaderboardName(); + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantagesByWindAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantagesByWindAction.java index d57753ab0ec..4d4d0ee8d67 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantagesByWindAction.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetStartlineAdvantagesByWindAction.java @@ -5,22 +5,41 @@ package com.sap.sailing.dashboards.gwt.client.actions; * */ -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage.StartlineAdvantagesByWindCalculator; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; +import com.sap.sailing.dashboards.gwt.shared.dispatch.RequiresLiveRaceAndCachesMovingAverageAction; import com.sap.sailing.dashboards.gwt.shared.dto.StartlineAdvantagesWithMaxAndAverageDTO; -import com.sap.sse.gwt.client.async.AsyncAction; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; +import com.sap.sse.common.impl.MillisecondsTimePoint; -public class GetStartlineAdvantagesByWindAction implements AsyncAction { +public class GetStartlineAdvantagesByWindAction extends RequiresLiveRaceAndCachesMovingAverageAction { - private final RibDashboardServiceAsync ribDashboardService; - private final String leaderboardName; - - public GetStartlineAdvantagesByWindAction(RibDashboardServiceAsync ribDashboardService, String leaderboardName) { - this.ribDashboardService = ribDashboardService; - this.leaderboardName = leaderboardName; + public GetStartlineAdvantagesByWindAction() {} + + public GetStartlineAdvantagesByWindAction(String leaderboardName) { + super(leaderboardName); + } + + @Override + @GwtIncompatible + public StartlineAdvantagesWithMaxAndAverageDTO execute(DashboardDispatchContext dashboardDispatchContext) throws DispatchException { + StartlineAdvantagesWithMaxAndAverageDTO result = new StartlineAdvantagesWithMaxAndAverageDTO(); + TrackedRace liveRace = super.getLiveRace(dashboardDispatchContext); + if (liveRace != null) { + StartlineAdvantagesByWindCalculator startlineAdvantagesByWindCalculator = new StartlineAdvantagesByWindCalculator(dashboardDispatchContext); + result = startlineAdvantagesByWindCalculator.getStartLineAdvantagesAccrossLineFromTrackedRaceAtTimePoint(liveRace, MillisecondsTimePoint.now()); + } + if(result != null && result.maximum != null) { + super.addValueToMovingAverage(result.maximum, dashboardDispatchContext.getMovingAveragesCache()); + result.average = dashboardDispatchContext.getMovingAveragesCache().getValueForKey(getKeyForMovingAverage()); + } + return result; } @Override - public void execute(AsyncCallback callback) { - ribDashboardService.getAdvantagesOnStartline(leaderboardName, callback); - }} \ No newline at end of file + protected String getKeyForMovingAverage() { + return super.getLeaderboardName(); + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java index 66008916713..6c97ddda791 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java @@ -9,14 +9,14 @@ import java.util.logging.Level; import java.util.logging.Logger; import com.google.gwt.user.client.rpc.AsyncCallback; -import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; +import com.sap.sailing.dashboards.gwt.client.DashboardClientFactory; import com.sap.sailing.dashboards.gwt.client.actions.GetIDFromRaceThatTakesWindFixesNowAction; import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; +import com.sap.sailing.dashboards.gwt.shared.dto.RaceIdDTO; 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.gwt.ui.actions.GetWindInfoAction; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.player.TimeListener; @@ -24,8 +24,6 @@ import com.sap.sse.gwt.client.player.TimeListener; public class WindBotDataRetriever implements TimeListener, WindBotDataRetrieverProvider { private AsyncActionsExecutor asyncActionsExecutor; - private final RibDashboardServiceAsync ribDashboardService; - private final SailingServiceAsync sailingService; private List numberOfWindBotsChangeListeners; private List windBotDataRetrieverListeners; private List windBotIDsInLiveRace; @@ -36,11 +34,12 @@ public class WindBotDataRetriever implements TimeListener, WindBotDataRetrieverP private final String LODA_WIND_CHART_DATA_CATEGORY = "loadWindChartData"; private boolean didInitialLoading; + private DashboardClientFactory dashboardClientFactory; + private static final Logger logger = Logger.getLogger(WindBotDataRetriever.class.getName()); - public WindBotDataRetriever(RibDashboardServiceAsync ribDashboardService, SailingServiceAsync sailingService) { - this.ribDashboardService = ribDashboardService; - this.sailingService = sailingService; + public WindBotDataRetriever(DashboardClientFactory dashboardClientFactory) { + this.dashboardClientFactory = dashboardClientFactory; didInitialLoading = false; asyncActionsExecutor = new AsyncActionsExecutor(); numberOfWindBotsChangeListeners = new ArrayList(); @@ -56,7 +55,7 @@ public class WindBotDataRetriever implements TimeListener, WindBotDataRetrieverP from = new Date(from.getTime()-ONE_HOUR_IN_MILLISECONDS); } logger.log(Level.INFO, "Executing GetWindInfoAction with from "+from+" and to "+to); - GetWindInfoAction getWindInfoAction = new GetWindInfoAction(sailingService, selectedRaceIdentifier, from, to, + GetWindInfoAction getWindInfoAction = new GetWindInfoAction(dashboardClientFactory.getSailingService(), selectedRaceIdentifier, from, to, WIND_CHART_RESOLUTION_IN_MILLISECONDS, windSourceTypeNames, /* * onlyUpToNewestEvent==true because we don't want to * overshoot the evidence so far @@ -143,26 +142,31 @@ public class WindBotDataRetriever implements TimeListener, WindBotDataRetrieverP public void timeChanged(Date newTime, Date oldTime) { final Date finalNewTime = newTime; final Date finaloldTime = oldTime; - GetIDFromRaceThatTakesWindFixesNowAction getIDFromRaceThatTakesWindFixesNowAction = new GetIDFromRaceThatTakesWindFixesNowAction(ribDashboardService, DashboardURLParameters.LEADERBOARD_NAME.getValue()); logger.log(Level.INFO, "Executing GetIDFromRaceThatTakesWindFixesNowAction"); - asyncActionsExecutor.execute(getIDFromRaceThatTakesWindFixesNowAction, new AsyncCallback() { - @Override - public void onSuccess(RegattaAndRaceIdentifier result) { - logger.log(Level.INFO, "Received RegattaAndRaceIdentifier from race that takes wind fixes now"); - if(result != null) { - loadWindBotData(finaloldTime, finalNewTime, result); - } else { - notifyListenersAboutNumberOfWindBotsReceivedIsZero(); - logger.log(Level.INFO, "Can´t load wind data because RegattaAndRaceIdentifier for race which takes wind is null"); - } - } + dashboardClientFactory.getDispatch().execute( + new GetIDFromRaceThatTakesWindFixesNowAction(DashboardURLParameters.LEADERBOARD_NAME.getValue()), + new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - notifyListenersAboutNumberOfWindBotsReceivedIsZero(); - logger.log(Level.INFO, " Failed to received RegattaAndRaceIdentifier from race that takes wind fixes now"); - logger.log(Level.INFO, caught.getMessage()); - } - }); + @Override + public void onSuccess(RaceIdDTO result) { + if (result != null) { + RegattaAndRaceIdentifier raceId = result.getRaceId(); + if (raceId != null) { + logger.log(Level.INFO, "Received RegattaAndRaceIdentifier from race that takes wind fixes now"); + loadWindBotData(finaloldTime, finalNewTime, raceId); + } else { + notifyListenersAboutNumberOfWindBotsReceivedIsZero(); + logger.log(Level.INFO, "Can´t load wind data because RegattaAndRaceIdentifier for race which takes wind is null"); + } + } + } + + @Override + public void onFailure(Throwable caught) { + notifyListenersAboutNumberOfWindBotsReceivedIsZero(); + logger.log(Level.INFO, "Failed to received RegattaAndRaceIdentifier from race that takes wind fixes now"); + logger.log(Level.INFO, caught.getMessage()); + } + }); } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java index 438a9dc55a7..4ec30c16e46 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java @@ -23,8 +23,10 @@ import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.dashboards.gwt.client.DashboardClientFactory; import com.sap.sailing.dashboards.gwt.client.RibDashboardImageResources; -import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; +import com.sap.sailing.dashboards.gwt.client.actions.GetCompetitorInLeaderboardAction; +import com.sap.sailing.dashboards.gwt.client.actions.GetStartAnalysesAction; import com.sap.sailing.dashboards.gwt.client.notifications.BottomNotification; import com.sap.sailing.dashboards.gwt.client.notifications.BottomNotificationClickListener; import com.sap.sailing.dashboards.gwt.client.notifications.BottomNotificationType; @@ -32,9 +34,10 @@ import com.sap.sailing.dashboards.gwt.client.popups.competitorselection.Competit import com.sap.sailing.dashboards.gwt.client.popups.competitorselection.CompetitorSelectionPopup; import com.sap.sailing.dashboards.gwt.client.popups.competitorselection.SettingsButtonWithSelectionIndicationLabel; import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; +import com.sap.sailing.dashboards.gwt.shared.dto.LeaderboardCompetitorsDTO; +import com.sap.sailing.dashboards.gwt.shared.dto.StartAnalysesDTO; import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.gwt.ui.client.SailingServiceAsync; 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; @@ -104,13 +107,13 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets private List pageChangeListener; private BottomNotification bottomNotification; private Timer timer; - private RibDashboardServiceAsync ribDashboardServiceAsync; - private SailingServiceAsync sailingServiceAsync; private static final int RERFRESH_INTERVAL = 10000; private CompetitorSelectionPopup competitorSelectionPopup; private SettingsButtonWithSelectionIndicationLabel settingsButtonWithSelectionIndicationLabel; + + private DashboardClientFactory dashboardClientFactory; private static final String SELECTED_COMPETITOR_ID_COOKIE_KEY = "selectedCompetitorId"; private static final int SELECTED_COMPETITOR_ID_COOKIE_KEY_EXPIRE_TIME_IN_MILLIS = 60 * 1000 * 60 * 5; @@ -120,9 +123,8 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets /** * Component that contains handles, displays and loads startanalysis cards. * */ - public StartlineAnalysisComponent(RibDashboardServiceAsync ribDashboardServiceAsync, SailingServiceAsync sailingServiceAsync) { - this.ribDashboardServiceAsync = ribDashboardServiceAsync; - this.sailingServiceAsync = sailingServiceAsync; + public StartlineAnalysisComponent(DashboardClientFactory dashboardClientFactory) { + this.dashboardClientFactory = dashboardClientFactory; pageChangeListener = new ArrayList(); starts = new ArrayList(); @@ -172,34 +174,31 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets private void loadStartAnalysisDTOsForCompetitorID(String competitorIdAsString){ logger.log(Level.INFO, "Loading startanalysis for competitor id " + competitorIdAsString); - ribDashboardServiceAsync.getStartAnalysisListForCompetitorIDAndLeaderboardName( - competitorIdAsString, DashboardURLParameters.LEADERBOARD_NAME.getValue(), new AsyncCallback>() { - @Override - public void onSuccess(List result) { - logger.log(Level.INFO, "Received startanalysis list"); - if (result != null && !result.isEmpty()) { - - if (displayedStartAnalysisCompetitorDifferentToRequestedOne()) { - removeAllStartAnalysisCards(); - } - - if (result.size() != starts.size()) { - showNotificationForNewStartAnalysis(); - } - logger.log(Level.INFO, "Updating UI with startanalysis list"); - addNewStartAnalysisCards(result); - settingsButtonWithSelectionIndicationLabel - .setSelectionIndicationTextOnLabel(result.get(0).competitor.getName()); - } else { - logger.log(Level.INFO, "Received startanalysis list is null or empty"); - } - } + dashboardClientFactory.getDispatch().execute(new GetStartAnalysesAction(DashboardURLParameters.LEADERBOARD_NAME.getValue(), competitorIdAsString), new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - logger.log(Level.INFO, "Failed to received startanalysis list, "+caught.getMessage()); - } - }); + @Override + public void onFailure(Throwable caught) { + logger.log(Level.INFO, "Failed to received startanalysis list, "+caught.getMessage()); + } + + @Override + public void onSuccess(StartAnalysesDTO result) { + logger.log(Level.INFO, "Received startanalysis list"); + if (result != null && !result.getStartAnalyses().isEmpty()) { + if (displayedStartAnalysisCompetitorDifferentToRequestedOne()) { + removeAllStartAnalysisCards(); + } + if (result.getStartAnalyses().size() != starts.size()) { + showNotificationForNewStartAnalysis(); + } + logger.log(Level.INFO, "Updating UI with startanalysis list"); + addNewStartAnalysisCards(result.getStartAnalyses()); + settingsButtonWithSelectionIndicationLabel.setSelectionIndicationTextOnLabel(result.getStartAnalyses().get(0).competitor.getName()); + } else { + logger.log(Level.INFO, "Received startanalysis list is null or empty"); + } + } + }); } private void initAndAddBottomNotification() { @@ -243,27 +242,28 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets private void loadCompetitorsAndShowCompetitorSelectionPopup() { logger.log(Level.INFO, "Requesting Competitors in Leaderboard"); - ribDashboardServiceAsync.getCompetitorsInLeaderboard(DashboardURLParameters.LEADERBOARD_NAME.getValue(), new AsyncCallback>() { - @Override - public void onSuccess(List result) { - logger.log(Level.INFO, "Received competitors for leaderboard name"); - if (result != null && result.size() > 0) { - if (!competitorSelectionPopup.isShown()) { - logger.log(Level.INFO, "Showing CompetitorSelectionPopup with competitors"); - competitorSelectionPopup.show(result); - } else { - logger.log(Level.INFO, "CompetitorSelectionPopup aleady shown"); - } - } else { - logger.log(Level.INFO, "Received competitors are null or empty"); - } - } + dashboardClientFactory.getDispatch().execute(new GetCompetitorInLeaderboardAction(DashboardURLParameters.LEADERBOARD_NAME.getValue()), new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - logger.log(Level.INFO, "Failed to received competitors for leaderboard name, "+caught.getMessage()); - } - }); + @Override + public void onSuccess(LeaderboardCompetitorsDTO result) { + logger.log(Level.INFO, "Received competitors for leaderboard name"); + if (result.getCompetitors() != null && result.getCompetitors().size() > 0) { + if (!competitorSelectionPopup.isShown()) { + logger.log(Level.INFO, "Showing CompetitorSelectionPopup with competitors"); + competitorSelectionPopup.show(result.getCompetitors()); + } else { + logger.log(Level.INFO, "CompetitorSelectionPopup aleady shown"); + } + } else { + logger.log(Level.INFO, "Received competitors are null or empty"); + } + } + + @Override + public void onFailure(Throwable caught) { + logger.log(Level.INFO, "Failed to received competitors for leaderboard name, "+caught.getMessage()); + } + }); } private void initAndAddSettingsButtonWithSelectionIndicationLabel() { @@ -349,7 +349,7 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets } final StartlineAnalysisCard startlineAnalysisCard = new StartlineAnalysisCard(numberOfStartAnalysisCards * SCROLL_OFFSET_STARTANALYSIS_CARDS + 10, numberOfStartAnalysisCards, startAnalysisDTO, - sailingServiceAsync); + dashboardClientFactory.getSailingService()); startanalysis_card_container.add(startlineAnalysisCard); startlineAnalysisCard.startAnalysisComponentPageChangedToIndexAndStartAnalysis(page, startAnalysisDTO); registerPageChangeListener(startlineAnalysisCard); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartLineAdvantageByGeometryComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartLineAdvantageByGeometryComponent.java index fe16319c68e..9e6c8ee4441 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartLineAdvantageByGeometryComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartLineAdvantageByGeometryComponent.java @@ -1,13 +1,19 @@ package com.sap.sailing.dashboards.gwt.client.startlineadvantage; +import java.util.Date; import java.util.logging.Level; import java.util.logging.Logger; import com.google.gwt.i18n.client.NumberFormat; -import com.sap.sailing.dashboards.gwt.client.dataretriever.RibDashboardDataRetriever; -import com.sap.sailing.dashboards.gwt.client.dataretriever.RibDashboardDataRetrieverListener; -import com.sap.sailing.dashboards.gwt.shared.dto.RibDashboardRaceInfoDTO; +import com.google.gwt.user.client.rpc.AsyncCallback; +import com.sap.sailing.dashboards.gwt.client.DashboardClientFactory; +import com.sap.sailing.dashboards.gwt.client.actions.GetStartlineAdvantageByGeometryAction; +import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; +import com.sap.sailing.dashboards.gwt.shared.dto.StartLineAdvantageDTO; import com.sap.sailing.gwt.ui.client.StringMessages; +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; /** * The purpose of the class is to display the live and average start line advantage either by wind or by geometry. There @@ -19,13 +25,16 @@ import com.sap.sailing.gwt.ui.client.StringMessages; * @author Alexander Ries (D062114) * */ -public class StartLineAdvantageByGeometryComponent extends LiveAverageComponent implements RibDashboardDataRetrieverListener { +public class StartLineAdvantageByGeometryComponent extends LiveAverageComponent implements TimeListener { + private DashboardClientFactory dashboardClientFactory; + private static StringMessages stringConstants = StringMessages.INSTANCE; private static final Logger logger = Logger.getLogger(StartLineAdvantageByGeometryComponent.class.getName()); - public StartLineAdvantageByGeometryComponent(RibDashboardDataRetriever ribDashboardDataRetriever) { + public StartLineAdvantageByGeometryComponent(DashboardClientFactory dashboardClientFactory) { super(stringConstants.dashboardStartlineAdvantageByGeometry(), "m"); + this.dashboardClientFactory = dashboardClientFactory; StartLineAdvantageComponentRessources.INSTANCE.css().ensureInjected(); this.header.setInnerText(stringConstants.dashboardStartlineAdvantageByGeometry()); this.header.addClassName(StartLineAdvantageComponentRessources.INSTANCE.css().startLineAdvantageComponent_header()); @@ -35,21 +44,40 @@ public class StartLineAdvantageByGeometryComponent extends LiveAverageComponent averagePanel.getElement().addClassName(StartLineAdvantageComponentRessources.INSTANCE.css().startLineAdvantageComponent_averagePanel()); this.liveLabel.setInnerHTML(stringConstants.dashboardLiveWind()); this.averageLabel.setInnerHTML(stringConstants.dashboardAverageWind()+"
    "+stringConstants.dashboardAverageWindMinutes(15)); - ribDashboardDataRetriever.addDataObserver(this); + initSampleTimer(); } - public void setStartLineAdvantageValues(RibDashboardRaceInfoDTO liveRaceInfoDTO) { - if (liveRaceInfoDTO.startLineAdvantageDTO != null) { - logger.log(Level.INFO, "Updating UI with RibDashboardRaceInfoDTO.startLineAdvantageDTO"); - setLiveValue(NumberFormat.getFormat("#0.0").format(liveRaceInfoDTO.startLineAdvantageDTO.startLineAdvantage)); - setAverageValue(NumberFormat.getFormat("#0.0").format(liveRaceInfoDTO.startLineAdvantageDTO.average)); - } else { - logger.log(Level.INFO, "RibDashboardRaceInfoDTO.startLineAdvantageDTO is null"); - } + private void initSampleTimer(){ + Timer timer = new Timer(PlayModes.Live); + timer.setRefreshInterval(5000); + timer.addTimeListener(this); + timer.play(); } @Override - public void updateUIWithNewLiveRaceInfo(RibDashboardRaceInfoDTO liveRaceInfoDTO) { - setStartLineAdvantageValues(liveRaceInfoDTO); + public void timeChanged(Date newTime, Date oldTime) { + String leaderboardNameParameterValue = DashboardURLParameters.LEADERBOARD_NAME.getValue(); + if (leaderboardNameParameterValue != null) { + dashboardClientFactory.getDispatch().execute( + new GetStartlineAdvantageByGeometryAction(leaderboardNameParameterValue), + new AsyncCallback() { + + @Override + public void onFailure(Throwable caught) { + logger.log(Level.INFO, "Failed to received startline advantage by geometry"); + } + + @Override + public void onSuccess(StartLineAdvantageDTO result) { + if (result.startLineAdvantage != null) { + logger.log(Level.INFO, "Updating UI with RibDashboardRaceInfoDTO.startLineAdvantageDTO"); + setLiveValue(NumberFormat.getFormat("#0.0").format(result.startLineAdvantage.doubleValue())); + setAverageValue(NumberFormat.getFormat("#0.0").format(result.average.doubleValue())); + } else { + logger.log(Level.INFO, "RibDashboardRaceInfoDTO.startLineAdvantageDTO is 0"); + } + } + }); + } } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java index 25d7be9e90f..dd211ec9127 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java @@ -15,12 +15,11 @@ import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; +import com.sap.sailing.dashboards.gwt.client.DashboardClientFactory; import com.sap.sailing.dashboards.gwt.client.actions.GetStartlineAdvantagesByWindAction; -import com.sap.sailing.dashboards.gwt.client.dataretriever.WindBotDataRetriever; +import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; import com.sap.sailing.dashboards.gwt.shared.dto.StartlineAdvantagesWithMaxAndAverageDTO; import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; 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; @@ -50,55 +49,59 @@ public class StartlineAdvantagesByWindComponent extends Composite implements Has @UiField StartlineAdvantagesByWindComponentStyle style; - private RibDashboardServiceAsync ribDashboardService; - private AsyncActionsExecutor asyncActionsExecutor; - private static final Logger logger = Logger.getLogger(WindBotDataRetriever.class.getName()); + public DashboardClientFactory dashboardClientFactory; + private static final Logger logger = Logger.getLogger(StartlineAdvantagesByWindComponent.class.getName()); - public StartlineAdvantagesByWindComponent(RibDashboardServiceAsync ribDashboardService) { + public StartlineAdvantagesByWindComponent(DashboardClientFactory dashboardClientFactory) { + this.dashboardClientFactory = dashboardClientFactory; startlineAdvantagesOnLineChart = new StartlineAdvantagesOnLineChart(); advantageMaximumLiveAverage = new LiveAverageComponent(StringMessages.INSTANCE.dashboardStartlineAdvantagesByWind(), "m"); advantageMaximumLiveAverage.header.getStyle().setFontSize(14, Unit.PT); advantageMaximumLiveAverage.liveLabel.setInnerHTML("advantage max."); advantageMaximumLiveAverage.averageLabel.setInnerHTML("advantage max. average "+StringMessages.INSTANCE.dashboardAverageWindMinutes(15)); initWidget(uiBinder.createAndBindUi(this)); - this.ribDashboardService = ribDashboardService; - this.asyncActionsExecutor = new AsyncActionsExecutor(); initSampleTimer(); } private void loadData() { - GetStartlineAdvantagesByWindAction getRibDashboardRaceInfoAction = new GetStartlineAdvantagesByWindAction(ribDashboardService, ""); logger.log(Level.INFO, "Executing GetStartlineAdvantagesAction"); - asyncActionsExecutor.execute(getRibDashboardRaceInfoAction, - new AsyncCallback() { + String leaderboardNameParameterValue = DashboardURLParameters.LEADERBOARD_NAME.getValue(); + if (leaderboardNameParameterValue != null) { + dashboardClientFactory.getDispatch().execute( + new GetStartlineAdvantagesByWindAction(leaderboardNameParameterValue), + new AsyncCallback() { - @Override - public void onSuccess(StartlineAdvantagesWithMaxAndAverageDTO result) { - logger.log(Level.INFO, "Received StartlineAdvantagesWithMaxAndAverageDTO"); - if (result != null) { - logger.log(Level.INFO, "Updating UI with StartlineAdvantagesWithMaxAndAverageDTO"); - startlineAdvantagesOnLineChart.setStartlineAdvantages(result.advantages); - advantageMaximumLiveAverage.setLiveValue(NumberFormat.getFormat("#0.0").format(result.maximum)); - advantageMaximumLiveAverage.setAverageValue(NumberFormat.getFormat("#0.0").format(result.average)); - } else { - logger.log(Level.INFO, "StartlineAdvantagesWithMaxAndAverageDTO is null"); + @Override + public void onSuccess(StartlineAdvantagesWithMaxAndAverageDTO result) { + logger.log(Level.INFO, "Received StartlineAdvantagesWithMaxAndAverageDTO"); + if (result != null && result.maximum != null && result.average != null) { + logger.log(Level.INFO, "Updating UI with StartlineAdvantagesWithMaxAndAverageDTO"); + startlineAdvantagesOnLineChart.setStartlineAdvantages(result.advantages); + advantageMaximumLiveAverage.setLiveValue(NumberFormat.getFormat("#0.0").format( + result.maximum.doubleValue())); + advantageMaximumLiveAverage.setAverageValue(NumberFormat.getFormat("#0.0").format( + result.average.doubleValue())); + } else { + logger.log(Level.INFO, "StartlineAdvantagesWithMaxAndAverageDTO is null"); + } } - } - @Override - public void onFailure(Throwable caught) { - logger.log(Level.INFO, "Failed to received StartlineAdvantagesWithMaxAndAverageDTO, "+caught.getMessage()); - } - }); + @Override + public void onFailure(Throwable caught) { + logger.log(Level.INFO, "Failed to received StartlineAdvantagesWithMaxAndAverageDTO, " + + caught.getMessage()); + } + }); + } } - - private void initSampleTimer(){ + + private void initSampleTimer() { Timer timer = new Timer(PlayModes.Live); timer.setRefreshInterval(5000); timer.addTimeListener(this); timer.play(); } - + @Override public void add(Widget w) { throw new UnsupportedOperationException("The method add(Widget w) is not supported."); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/DashboardDispatchRPCServletImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/DashboardDispatchRPCServletImpl.java index 8e9ea5781e5..7f7533190d9 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/DashboardDispatchRPCServletImpl.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/DashboardDispatchRPCServletImpl.java @@ -5,10 +5,14 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; import org.osgi.util.tracker.ServiceTracker; +import com.sap.sailing.dashboards.gwt.shared.DashboardLiveRaceProvider; +import com.sap.sailing.dashboards.gwt.shared.MovingAveragesCache; import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; import com.sap.sailing.dashboards.gwt.shared.dispatch.impl.DashboardDispatchContextImpl; +import com.sap.sailing.domain.polars.PolarDataService; import com.sap.sailing.gwt.dispatch.client.Action; import com.sap.sailing.gwt.dispatch.client.Result; import com.sap.sailing.gwt.dispatch.client.ResultWrapper; @@ -26,11 +30,16 @@ public class DashboardDispatchRPCServletImpl extends ProxiedRemoteServiceServlet private static final Logger logger = Logger.getLogger(DashboardDispatchRPCServletImpl.class.getName()); private final ServiceTracker racingEventServiceTracker; + private final PolarDataService polarDataService; + private final DashboardLiveRaceProvider dashboardLiveRaceProvider; + private final MovingAveragesCache movingAveragesCache; public DashboardDispatchRPCServletImpl() { final BundleContext context = Activator.getDefault(); - racingEventServiceTracker = ServiceTrackerFactory.createAndOpen(context, RacingEventService.class); + polarDataService = getPolarDataService(); + dashboardLiveRaceProvider = new DashboardLiveRaceProvider(racingEventServiceTracker.getService()); + movingAveragesCache = new MovingAveragesCache(); } @Override @@ -39,10 +48,9 @@ public class DashboardDispatchRPCServletImpl extends ProxiedRemoteServiceServlet A action = request.getAction(); long start = System.currentTimeMillis(); try { - R executionResult = action.execute( - new DashboardDispatchContextImpl(request.getCurrentClientTime(), - racingEventServiceTracker.getService(), request - .getClientLocaleName(), getThreadLocalRequest())); + R executionResult = action.execute(new DashboardDispatchContextImpl(request.getCurrentClientTime(), + racingEventServiceTracker.getService(), polarDataService, dashboardLiveRaceProvider, + movingAveragesCache, request.getClientLocaleName(), getThreadLocalRequest())); return new ResultWrapper(executionResult); } catch (DispatchException d) { logger.log(Level.WARNING, "Server exception", d); @@ -70,4 +78,10 @@ public class DashboardDispatchRPCServletImpl extends ProxiedRemoteServiceServlet logger.log(Level.WARNING, "GWT RPC Exception: " + e.getMessage(), e); super.doUnexpectedFailure(e); } + + private PolarDataService getPolarDataService() { + BundleContext context = Activator.getDefault(); + ServiceReference polarServiceReference = context.getServiceReference(PolarDataService.class); + return context.getService(polarServiceReference); + } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/LiveTrackedRaceListener.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/LiveTrackedRaceListener.java deleted file mode 100644 index c67872c5d89..00000000000 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/LiveTrackedRaceListener.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.sap.sailing.dashboards.gwt.server; - -import com.sap.sailing.domain.tracking.TrackedRace; - -/** - * Informs the listener about changes in a certain environment, where a {@link TrackedRace} with - * {@link TrackedRace#getStatus()} live is continuously required. - * - * @author Alexander Ries (D062114) - * - */ -public interface LiveTrackedRaceListener { - - public void liveTrackedRaceDidChange(TrackedRace trackedRace); -} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/LiveTrackedRaceProvider.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/LiveTrackedRaceProvider.java deleted file mode 100644 index 71dc43a4589..00000000000 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/LiveTrackedRaceProvider.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.sap.sailing.dashboards.gwt.server; - -import java.util.ArrayList; -import java.util.List; - -import com.sap.sailing.domain.tracking.TrackedRace; - -/** - * Is continuously looking for {@link TrackedRace} with {@link TrackedRace#getStatus()} live and - * notifies {@link LiveTrackedRaceListener} if the current live {@link TrackedRace} has changed. - * - * - * @author Alexander Ries (D062114) - * - */ -public interface LiveTrackedRaceProvider { - - List listener = new ArrayList(); - - public void notifyLiveTrackedRaceListenerAboutLiveTrackedRaceChange(TrackedRace trackedRace); - - default void addLiveTrackedRaceListener(LiveTrackedRaceListener liveTrackedRaceListener){ - listener.add(liveTrackedRaceListener); - } - - default void removeLiveTrackedRaceListener(LiveTrackedRaceListener liveTrackedRaceListener){ - listener.remove(liveTrackedRaceListener); - } -} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/RibDashboardServiceImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/RibDashboardServiceImpl.java deleted file mode 100644 index e79d721f70b..00000000000 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/RibDashboardServiceImpl.java +++ /dev/null @@ -1,331 +0,0 @@ -package com.sap.sailing.dashboards.gwt.server; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceReference; -import org.osgi.util.tracker.ServiceTracker; - -import com.google.gwt.user.server.rpc.RemoteServiceServlet; -import com.sap.sailing.dashboards.gwt.client.RibDashboardService; -import com.sap.sailing.dashboards.gwt.client.startanalysis.StartlineAdvantageType; -import com.sap.sailing.dashboards.gwt.server.startanalysis.StartAnalysisCreationController; -import com.sap.sailing.dashboards.gwt.server.startlineadvantages.StartlineAdvantagesCalculator; -import com.sap.sailing.dashboards.gwt.shared.MovingAverage; -import com.sap.sailing.dashboards.gwt.shared.ResponseMessage; -import com.sap.sailing.dashboards.gwt.shared.dto.RibDashboardRaceInfoDTO; -import com.sap.sailing.dashboards.gwt.shared.dto.StartLineAdvantageDTO; -import com.sap.sailing.dashboards.gwt.shared.dto.StartlineAdvantagesWithMaxAndAverageDTO; -import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisDTO; -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.Course; -import com.sap.sailing.domain.base.Fleet; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.base.Waypoint; -import com.sap.sailing.domain.common.Distance; -import com.sap.sailing.domain.common.NoWindException; -import com.sap.sailing.domain.common.Position; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.dto.CompetitorDTO; -import com.sap.sailing.domain.common.tracking.GPSFix; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.domain.polars.PolarDataService; -import com.sap.sailing.domain.tracking.GPSFixTrack; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.server.RacingEventService; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.Util.Pair; -import com.sap.sse.common.impl.MillisecondsTimePoint; - -/** - * The server side implementation of the RPC {@link RibDashboardService}. - * - * @author Alexander Ries (D062114) - * - */ -public class RibDashboardServiceImpl extends RemoteServiceServlet implements RibDashboardService, LiveTrackedRaceProvider { - - /** - * - */ - private static final long serialVersionUID = 1L; - - private StartAnalysisCreationController startAnalysisCreationController; - - private StartlineAdvantagesCalculator startlineAdvantagesCalculator; - - /** - * Variable contains last {@link TrackedRace} that is or was live - * */ - private TrackedRace runningRace; - - /** - * {@link MovingAverage} of last 400 start line advantage values during the last races. See initialization at - * {@link #RibDashboardServiceImpl()}) Advantage by geometry is the delta in meters of the distance from race - * committee boat to first mark and pin end mark to first mark. Wind has no influence on the value. - * */ - private MovingAverage averageStartLineAdvantageByGeometry; - - private final ServiceTracker racingEventServiceTracker; - private final com.sap.sailing.domain.base.DomainFactory baseDomainFactory; - private final BundleContext context; - - private static final Logger logger = Logger.getLogger(RibDashboardServiceImpl.class.getName()); - - public RibDashboardServiceImpl() { - this.context = Activator.getDefault(); - this.racingEventServiceTracker = createAndOpenRacingEventServiceTracker(context); - this.baseDomainFactory = getRacingEventService().getBaseDomainFactory(); - this.startAnalysisCreationController = new StartAnalysisCreationController(getRacingEventService()); - this.startlineAdvantagesCalculator = new StartlineAdvantagesCalculator(getPolarService()); - addLiveTrackedRaceListener(startlineAdvantagesCalculator); - this.averageStartLineAdvantageByGeometry = new MovingAverage(400); - } - - @Override - public RegattaAndRaceIdentifier getIDFromRaceThatIsLive(String leaderboardName) { - RegattaAndRaceIdentifier result = null; - TrackedRace trackedRaceThatIsLive = getLiveRaceFromLeaderboardName(leaderboardName); - if(trackedRaceThatIsLive != null) { - result = trackedRaceThatIsLive.getRaceIdentifier(); - } - return result; - } - - protected RacingEventService getRacingEventService() { - return racingEventServiceTracker.getService(); - } - - private PolarDataService getPolarService() { - ServiceReference polarServiceReference = context.getServiceReference(PolarDataService.class); - PolarDataService polarDataService = context.getService(polarServiceReference); - return polarDataService; - } - - protected ServiceTracker createAndOpenRacingEventServiceTracker( - BundleContext context) { - ServiceTracker result = new ServiceTracker( - context, RacingEventService.class.getName(), null); - result.open(); - return result; - } - - /** - * leaderboardName is used to retrieve the live running race with the method - * {@link #getLiveRaceFromLeaderboardName(String)}. - * - * @throws NoWindException - * */ - @Override - public RibDashboardRaceInfoDTO getLiveRaceInfo(String leaderboardName) throws NoWindException { - RibDashboardRaceInfoDTO lRInfo = new RibDashboardRaceInfoDTO(); - if (leaderboardName != null) { - TimePoint timePointOfRequest = MillisecondsTimePoint.now(); - if (checkIfRaceIsStillRunning(timePointOfRequest, leaderboardName)) { - fillLiveRaceInfoDTOWithRaceData(lRInfo, timePointOfRequest); - fillLiveRaceInfoDTOWithStartLineAdavantageByGeometryData(lRInfo, timePointOfRequest); - lRInfo.responseMessage = ResponseMessage.RACE_LIVE; - } else { - lRInfo.responseMessage = ResponseMessage.NO_RACE_LIVE; - } - } else { - lRInfo.responseMessage = ResponseMessage.NO_RACE_LIVE; - } - return lRInfo; - } - - @Override - public RegattaAndRaceIdentifier getIDFromRaceThatTakesWindFixesNow(String leaderboardName) { - RegattaAndRaceIdentifier result = null; - if(leaderboardName != null) { - List trackedRaces = getTrackedRacesFromLeaderboard(leaderboardName); - List filteredTrackedRaces = trackedRaces.stream().filter(trackedRace -> trackedRace.takesWindFixWithTimePoint(MillisecondsTimePoint.now())).collect(Collectors.toList()); - if(filteredTrackedRaces != null && filteredTrackedRaces.size() > 0) { - TrackedRace raceThatTakesWindNow = filteredTrackedRaces.get(0); - if(raceThatTakesWindNow != null) { - result = raceThatTakesWindNow.getRaceIdentifier(); - } - } - } - return result; - } - - private void fillLiveRaceInfoDTOWithRaceData(RibDashboardRaceInfoDTO lRInfo, TimePoint now) { - if (runningRace != null) { - List competitors = runningRace.getCompetitorsFromBestToWorst(now); - List competitorNames = new ArrayList(); - for (Competitor competitor : competitors) { - competitorNames.add(competitor.getName()); - } - Collections.sort(competitorNames); - lRInfo.competitorNamesFromLastTrackedRace = competitorNames; - } - } - - // returns true if race is still live - private boolean checkIfRaceIsStillRunning(TimePoint now, String leaderboardName) { - if (runningRace == null || !(runningRace.isLive(now))) { - runningRace = getLiveRaceFromLeaderboardName(leaderboardName); - if (runningRace == null) { - return false; - } else { - return true; - } - } else { - return true; - } - } - - private TrackedRace getLiveRaceFromLeaderboardName(String leaderboardName) { - TrackedRace result = null; - Leaderboard leaderboard = getRacingEventService().getLeaderboardByName(leaderboardName); - if (leaderboard != null) { - for (RaceColumn column : leaderboard.getRaceColumns()) { - for (Fleet fleet : column.getFleets()) { - TrackedRace race = column.getTrackedRace(fleet); - if (race != null && race.isLive(MillisecondsTimePoint.now())) { - result = race; - } - } - } - } - return result; - } - - private List getTrackedRacesFromLeaderboard(String leaderboardName) { - List result = new ArrayList(); - Leaderboard lb = getRacingEventService().getLeaderboardByName(leaderboardName); - if (lb != null) { - for (RaceColumn column : lb.getRaceColumns()) { - for (Fleet fleet : column.getFleets()) { - TrackedRace race = column.getTrackedRace(fleet); - if (race != null) { - result.add(race); - } - } - } - } - return result; - } - - private void fillLiveRaceInfoDTOWithStartLineAdavantageByGeometryData(RibDashboardRaceInfoDTO lRInfo, - TimePoint timePoint) throws NoWindException { - if (runningRace != null && timePoint != null) { - StartLineAdvantageDTO startLineAdvantageDTO = new StartLineAdvantageDTO(); - startLineAdvantageDTO.startLineAdvatageType = StartlineAdvantageType.GEOMETRIC; - double startlineAdvantage = calculateStartlineAdvantageByGeometry(); - startLineAdvantageDTO.startLineAdvantage = startlineAdvantage; - averageStartLineAdvantageByGeometry.add(startlineAdvantage); - startLineAdvantageDTO.average = averageStartLineAdvantageByGeometry.getAverage(); - lRInfo.startLineAdvantageDTO = startLineAdvantageDTO; - } - } - - private double calculateStartlineAdvantageByGeometry() { - double result = 0.0; - Course course = runningRace.getRace().getCourse(); - if (course != null) { - Waypoint startlineWayPoint = course.getFirstLeg().getFrom(); - Waypoint firstmarkWayPoint = course.getFirstLeg().getTo(); - if (startlineWayPoint != null && firstmarkWayPoint != null) { - Pair startlineMarkPositions = retrieveStartlineMarkPositionsFromStartLineWayPoint(startlineWayPoint); - Position firstMarkPosition = retrieveFirstMarkPositionFromFirstMarkWayPoint(firstmarkWayPoint); - if (startlineMarkPositions != null && firstMarkPosition != null) { - Distance rcToMark = firstMarkPosition.getDistance(startlineMarkPositions.getA()); - Distance pinToMark = firstMarkPosition.getDistance(startlineMarkPositions.getB()); - return rcToMark.getMeters() - pinToMark.getMeters(); - } - } - } - return result; - } - - private Pair retrieveStartlineMarkPositionsFromStartLineWayPoint(Waypoint startLineWayPoint) { - Pair result = null; - Iterator markIterator = startLineWayPoint.getMarks().iterator(); - if (markIterator.hasNext()) { - Mark startboat = (Mark) markIterator.next(); - if (markIterator.hasNext()) { - Mark pinEnd = (Mark) markIterator.next(); - TimePoint now = MillisecondsTimePoint.now(); - Position startBoatPosition = getPositionFromMarkAtTimePoint(runningRace, startboat, now); - Position pinEndPosition = getPositionFromMarkAtTimePoint(runningRace, pinEnd, now); - if(startBoatPosition != null && pinEndPosition != null) { - result = new Pair(startBoatPosition, pinEndPosition); - } - } - } - return result; - } - - private Position getPositionFromMarkAtTimePoint(TrackedRace trackedRace, Mark mark, TimePoint timePoint) { - GPSFixTrack fixTrack = trackedRace.getTrack(mark); - return fixTrack.getEstimatedPosition(timePoint, true); - } - - private Position retrieveFirstMarkPositionFromFirstMarkWayPoint(Waypoint firstMarkWayPoint) { - Position firstMarkPosition = null; - if (firstMarkWayPoint.getMarks().iterator().hasNext()) { - Mark firstMark = firstMarkWayPoint.getMarks().iterator().next(); - TimePoint now = MillisecondsTimePoint.now(); - firstMarkPosition = getPositionFromMarkAtTimePoint(runningRace, firstMark, now); - logger.log(Level.INFO, "Firstmark: " + firstMarkPosition); - } - return firstMarkPosition; - } - - protected com.sap.sailing.domain.base.DomainFactory getBaseDomainFactory() { - return baseDomainFactory; - } - - @Override - public List getStartAnalysisListForCompetitorIDAndLeaderboardName(String competitorIdAsString, - String leaderboardName) { - List result = null; - if (competitorIdAsString != null && leaderboardName != null) { - result = new ArrayList(); - try { - Competitor competitor = baseDomainFactory.getCompetitorStore().getExistingCompetitorByIdAsString(competitorIdAsString); - List trackedRacesForLeaderBoardName = getTrackedRacesFromLeaderboard(leaderboardName); - for (TrackedRace trackedRace : trackedRacesForLeaderBoardName) { - StartAnalysisDTO startAnalysisDTO = startAnalysisCreationController.checkStartAnalysisForCompetitorInTrackedRace(competitor, trackedRace); - if (startAnalysisDTO != null) { - result.add(startAnalysisDTO); - } - } - } catch (NullPointerException e) { - logger.log(Level.INFO, "", e); - } - } - return result; - } - - @Override - public List getCompetitorsInLeaderboard(String leaderboardName) { - logger.log(Level.INFO, "getCompetitorsInLeaderboard(...) Request."); - if (leaderboardName != null) { - Leaderboard lb = getRacingEventService().getLeaderboardByName(leaderboardName); - return baseDomainFactory.getCompetitorDTOList(lb.getCompetitorsFromBestToWorst(MillisecondsTimePoint.now())); - } else { - return null; - } - } - - @Override - public StartlineAdvantagesWithMaxAndAverageDTO getAdvantagesOnStartline(String leaderboardName) { - return startlineAdvantagesCalculator.getStartLineAdvantagesAccrossLineAtTimePoint(MillisecondsTimePoint.now()); - } - - - @Override - public void notifyLiveTrackedRaceListenerAboutLiveTrackedRaceChange(TrackedRace trackedRace) { - listener.forEach(listener -> listener.liveTrackedRaceDidChange(trackedRace)); - } -} \ No newline at end of file diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startanalysis/AbstractStartAnalysisCreationValidator.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/AbstractStartAnalysisCreationValidator.java similarity index 79% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startanalysis/AbstractStartAnalysisCreationValidator.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/AbstractStartAnalysisCreationValidator.java index 8747a387975..1839f4b31ef 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startanalysis/AbstractStartAnalysisCreationValidator.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/AbstractStartAnalysisCreationValidator.java @@ -1,19 +1,21 @@ -package com.sap.sailing.dashboards.gwt.server.startanalysis; +package com.sap.sailing.dashboards.gwt.server.util.actions.startanalysis; import java.util.Iterator; +import com.google.gwt.core.shared.GwtIncompatible; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sse.common.TimePoint; +@GwtIncompatible public abstract class AbstractStartAnalysisCreationValidator { protected static int MINIMUM_MARKPASSIINGS_AT_FIRST_MARK = 3; protected static long MINIMUM_RACE_PROGRESSION_IN_MILLISECONDS = 6*60*1000; - protected boolean threeCompetitorsPassedSecondWayPoint(TrackedRace trackedRace){ + protected static boolean threeCompetitorsPassedSecondWayPoint(TrackedRace trackedRace){ Waypoint secondWaypoint = trackedRace.getRace().getCourse().getFirstLeg().getTo(); Iterator markPassingsInOrder = trackedRace.getMarkPassingsInOrder(secondWaypoint).iterator(); int markPassingsCounter = 0; @@ -24,12 +26,12 @@ public abstract class AbstractStartAnalysisCreationValidator { return markPassingsCounter >= MINIMUM_MARKPASSIINGS_AT_FIRST_MARK ? true : false; } - private boolean competitorPassedSecondWayPoint(Competitor competitor, TrackedRace trackedRace){ + private static boolean competitorPassedSecondWayPoint(Competitor competitor, TrackedRace trackedRace){ Waypoint secondWaypoint = trackedRace.getRace().getCourse().getFirstLeg().getTo(); return trackedRace.getMarkPassing(competitor, secondWaypoint) == null ? false : true; } - private boolean gateStartGolfFlagIsDown(TrackedRace trackedRace){ + private static boolean gateStartGolfFlagIsDown(TrackedRace trackedRace){ if(trackedRace.getGateStartGolfDownTime() != 0){ return true; }else{ @@ -44,7 +46,7 @@ public abstract class AbstractStartAnalysisCreationValidator { return false; } - protected boolean raceProgressedFarEnough(Competitor competitor, TrackedRace trackedRace){ + protected static boolean raceProgressedFarEnough(Competitor competitor, TrackedRace trackedRace){ final Boolean isGateStart = trackedRace.isGateStart(); if(isGateStart == Boolean.TRUE){ return gateStartGolfFlagIsDown(trackedRace); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startanalysis/StartAnalysisCreationController.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisCreationController.java similarity index 57% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startanalysis/StartAnalysisCreationController.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisCreationController.java index 35ff0fc276f..b2643f57543 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startanalysis/StartAnalysisCreationController.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisCreationController.java @@ -1,30 +1,29 @@ -package com.sap.sailing.dashboards.gwt.server.startanalysis; +package com.sap.sailing.dashboards.gwt.server.util.actions.startanalysis; import java.util.logging.Level; import java.util.logging.Logger; +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisDTO; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.server.RacingEventService; -public class StartAnalysisCreationController extends AbstractStartAnalysisCreationValidator { +@GwtIncompatible +public final class StartAnalysisCreationController extends AbstractStartAnalysisCreationValidator { - private StartAnalysisDTOFactory startAnalysisDTOFactory; - private static final Logger logger = Logger.getLogger(StartAnalysisCreationController.class.getName()); - public StartAnalysisCreationController(RacingEventService racingEventService) { - startAnalysisDTOFactory = new StartAnalysisDTOFactory(racingEventService); + private StartAnalysisCreationController(DashboardDispatchContext ctx) { + } - public StartAnalysisDTO checkStartAnalysisForCompetitorInTrackedRace(Competitor competitor, TrackedRace trackedRace) { + public static StartAnalysisDTO checkStartAnalysisForCompetitorInTrackedRace(DashboardDispatchContext dashboardDispatchContext, Competitor competitor, TrackedRace trackedRace) { StartAnalysisDTO result = null; if (competitor != null && trackedRace != null) { if (threeCompetitorsPassedSecondWayPoint(trackedRace) && raceProgressedFarEnough(competitor, trackedRace)) { logger.log(Level.INFO, "Creating startanalysis for race " + trackedRace.getRace().getName() + " and competitor: " + competitor.getName()); - result = startAnalysisDTOFactory.createStartAnalysisForCompetitorAndTrackedRace(competitor, trackedRace); - + result = StartAnalysisDTOFactory.createStartAnalysisForCompetitorAndTrackedRace(dashboardDispatchContext, competitor, trackedRace); } else { logger.log(Level.INFO, "Waiting to create startanalysis for race " + trackedRace.getRace().getName() + " and competitor: " + competitor.getName()); } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startanalysis/StartAnalysisDTOFactory.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisDTOFactory.java similarity index 79% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startanalysis/StartAnalysisDTOFactory.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisDTOFactory.java index 582ba70c694..b564638511d 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startanalysis/StartAnalysisDTOFactory.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisDTOFactory.java @@ -1,4 +1,4 @@ -package com.sap.sailing.dashboards.gwt.server.startanalysis; +package com.sap.sailing.dashboards.gwt.server.util.actions.startanalysis; import java.util.ArrayList; import java.util.Date; @@ -7,7 +7,9 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import com.google.gwt.core.shared.GwtIncompatible; import com.sap.sailing.dashboards.gwt.client.startanalysis.StartlineAdvantageType; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; import com.sap.sailing.dashboards.gwt.shared.dto.StartLineAdvantageDTO; import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisCompetitorDTO; import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisDTO; @@ -24,24 +26,20 @@ import com.sap.sailing.domain.tracking.LineDetails; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.TrackedLeg; import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sailing.server.RacingEventService; import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsTimePoint; -public class StartAnalysisDTOFactory extends AbstractStartAnalysisCreationValidator { - - private final com.sap.sailing.domain.base.DomainFactory baseDomainFactory; +@GwtIncompatible +public final class StartAnalysisDTOFactory extends AbstractStartAnalysisCreationValidator { private static int DEFAULT_GATE_START_INTERVALL_IN_MILLISECONDS = 5*60*1000; private static int ONE_MINUTE_INTERVALL_IN_MILLISECONDS = 60*1000; private static final Logger logger = Logger.getLogger(StartAnalysisDTOFactory.class.getName()); - public StartAnalysisDTOFactory(RacingEventService racingEventService) { - baseDomainFactory = racingEventService.getBaseDomainFactory(); - } + private StartAnalysisDTOFactory() {} - public StartAnalysisDTO createStartAnalysisForCompetitorAndTrackedRace(Competitor competitor, TrackedRace trackedRace) { + public static StartAnalysisDTO createStartAnalysisForCompetitorAndTrackedRace(DashboardDispatchContext dashboardDispatchContext, Competitor competitor, TrackedRace trackedRace) { StartAnalysisDTO startAnalysisDTO = new StartAnalysisDTO(); addStaticDataToStartAnalysisDTOFrom(startAnalysisDTO, trackedRace); List competitors = new ArrayList(); @@ -49,7 +47,7 @@ public class StartAnalysisDTOFactory extends AbstractStartAnalysisCreationValida List markPassingsInOrder = convertMarkpPassingsIteratorToList(trackedRace.getMarkPassingsInOrder(secondWaypoint).iterator()); boolean isCompetitorOneOfFirstThree = false; for (int i = 0; i < MINIMUM_MARKPASSIINGS_AT_FIRST_MARK; i++) { - competitors.add(createStartAnalysisCompetitorDTO(trackedRace, i + 1, markPassingsInOrder.get(i).getCompetitor())); + competitors.add(createStartAnalysisCompetitorDTO(dashboardDispatchContext, trackedRace, i + 1, markPassingsInOrder.get(i).getCompetitor())); if (markPassingsInOrder.get(i).getCompetitor().equals(competitor)) { isCompetitorOneOfFirstThree = true; } @@ -57,9 +55,9 @@ public class StartAnalysisDTOFactory extends AbstractStartAnalysisCreationValida if (!isCompetitorOneOfFirstThree) { int rankOfCompetitorWhilePassingSecondWaypoint = getRankOfCompetitorWhilePassingSecondWaypoint(competitor, trackedRace); - competitors.add(createStartAnalysisCompetitorDTO(trackedRace, rankOfCompetitorWhilePassingSecondWaypoint, competitor)); + competitors.add(createStartAnalysisCompetitorDTO(dashboardDispatchContext, trackedRace, rankOfCompetitorWhilePassingSecondWaypoint, competitor)); } - startAnalysisDTO.competitor = baseDomainFactory.getCompetitorStore().convertToCompetitorDTO(competitor); + startAnalysisDTO.competitor = dashboardDispatchContext.getRacingEventService().getBaseDomainFactory().getCompetitorStore().convertToCompetitorDTO(competitor); startAnalysisDTO.startAnalysisCompetitorDTOs = competitors; final Boolean isGateStart = trackedRace.isGateStart(); if(isGateStart == Boolean.TRUE){ @@ -83,7 +81,7 @@ public class StartAnalysisDTOFactory extends AbstractStartAnalysisCreationValida return startAnalysisDTO; } - private List convertMarkpPassingsIteratorToList(Iterator iterator) { + private static List convertMarkpPassingsIteratorToList(Iterator iterator) { List list = new ArrayList(); while (iterator.hasNext()) { list.add(iterator.next()); @@ -91,7 +89,7 @@ public class StartAnalysisDTOFactory extends AbstractStartAnalysisCreationValida return list; } - private int getRankOfCompetitorWhilePassingSecondWaypoint(Competitor competitor, TrackedRace trackedRace) { + private static int getRankOfCompetitorWhilePassingSecondWaypoint(Competitor competitor, TrackedRace trackedRace) { Waypoint secondWaypoint = trackedRace.getRace().getCourse().getFirstLeg().getTo(); Iterator markPassings = trackedRace.getMarkPassingsInOrder(secondWaypoint).iterator(); int counter = 0; @@ -103,20 +101,20 @@ public class StartAnalysisDTOFactory extends AbstractStartAnalysisCreationValida return 0; } - private StartAnalysisDTO addStaticDataToStartAnalysisDTOFrom(StartAnalysisDTO startAnalysisDTO, TrackedRace trackedRace) { + private static StartAnalysisDTO addStaticDataToStartAnalysisDTOFrom(StartAnalysisDTO startAnalysisDTO, TrackedRace trackedRace) { startAnalysisDTO.raceName = trackedRace.getRace().getName(); startAnalysisDTO.startAnalysisWindLineInfoDTO = createStartAnalysisWindAndLineData(trackedRace); return startAnalysisDTO; } - private StartAnalysisRankingTableEntryDTO createRankTableEntry(TrackedRace trackedRace, int rank, + private static StartAnalysisRankingTableEntryDTO createRankTableEntry(TrackedRace trackedRace, int rank, Competitor competitor) { StartAnalysisRankingTableEntryDTO startAnalysisRankTableEntryDTO = createStartAnalysisRankTableEntryDTOWithRankAndStartTimepoint( competitor, rank, trackedRace); return startAnalysisRankTableEntryDTO; } - private StartAnalysisRankingTableEntryDTO createStartAnalysisRankTableEntryDTOWithRankAndStartTimepoint( + private static StartAnalysisRankingTableEntryDTO createStartAnalysisRankTableEntryDTOWithRankAndStartTimepoint( Competitor competitor, int rank, TrackedRace trackedRace) { StartAnalysisRankingTableEntryDTO tableentry = new StartAnalysisRankingTableEntryDTO(); tableentry.rankAtFirstMark = rank; @@ -133,7 +131,7 @@ public class StartAnalysisDTOFactory extends AbstractStartAnalysisCreationValida return tableentry; } - private WindAndAdvantagesInfoForStartLineDTO createStartAnalysisWindAndLineData(TrackedRace trackedRace) { + private static WindAndAdvantagesInfoForStartLineDTO createStartAnalysisWindAndLineData(TrackedRace trackedRace) { WindAndAdvantagesInfoForStartLineDTO startAnalysisWindLineInfoDTO = new WindAndAdvantagesInfoForStartLineDTO(); final TimePoint startOfRace = trackedRace.getStartOfRace(); final TimePoint timePoint = startOfRace == null ? MillisecondsTimePoint.now() : startOfRace; @@ -158,7 +156,7 @@ public class StartAnalysisDTOFactory extends AbstractStartAnalysisCreationValida return startAnalysisWindLineInfoDTO; } - private StartlineAdvantageType getStartlineAdvantageType(TrackedRace trackedRace, TimePoint timePoint) { + private static StartlineAdvantageType getStartlineAdvantageType(TrackedRace trackedRace, TimePoint timePoint) { try { LegType typeOfFirstLeg; typeOfFirstLeg = getFirstLegTypeOfTrackedRaceAtTimePoint(trackedRace, timePoint); @@ -176,7 +174,7 @@ public class StartAnalysisDTOFactory extends AbstractStartAnalysisCreationValida } } - private LegType getFirstLegTypeOfTrackedRaceAtTimePoint(TrackedRace trackedRace, TimePoint timePoint) + private static LegType getFirstLegTypeOfTrackedRaceAtTimePoint(TrackedRace trackedRace, TimePoint timePoint) throws NoWindException { Iterable trackedLegs = trackedRace.getTrackedLegs(); if (trackedLegs != null && trackedLegs.iterator().hasNext()) { @@ -187,10 +185,10 @@ public class StartAnalysisDTOFactory extends AbstractStartAnalysisCreationValida } } - private StartAnalysisCompetitorDTO createStartAnalysisCompetitorDTO(TrackedRace trackedRace, int rank, + private static StartAnalysisCompetitorDTO createStartAnalysisCompetitorDTO(DashboardDispatchContext dashboardDispatchContext, TrackedRace trackedRace, int rank, Competitor competitor) { StartAnalysisCompetitorDTO startAnalysisCompetitorDTOsForRace = new StartAnalysisCompetitorDTO(); - startAnalysisCompetitorDTOsForRace.competitorDTO = baseDomainFactory.getCompetitorStore() + startAnalysisCompetitorDTOsForRace.competitorDTO = dashboardDispatchContext.getRacingEventService().getBaseDomainFactory().getCompetitorStore() .convertToCompetitorDTO(competitor); startAnalysisCompetitorDTOsForRace.rankingTableEntryDTO = createRankTableEntry(trackedRace, rank, competitor); return startAnalysisCompetitorDTOsForRace; diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/DefaultPolarValues.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/DefaultPolarValues.java similarity index 87% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/DefaultPolarValues.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/DefaultPolarValues.java index 721524ae6ae..955f215fef8 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/DefaultPolarValues.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/DefaultPolarValues.java @@ -1,4 +1,4 @@ -package com.sap.sailing.dashboards.gwt.server.startlineadvantages; +package com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage; import com.sap.sailing.domain.common.ManeuverType; diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/DefaultPolarWindAngleBoatSpeedFunction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/DefaultPolarWindAngleBoatSpeedFunction.java similarity index 96% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/DefaultPolarWindAngleBoatSpeedFunction.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/DefaultPolarWindAngleBoatSpeedFunction.java index 8c7082fdb43..7b16e26be3d 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/DefaultPolarWindAngleBoatSpeedFunction.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/DefaultPolarWindAngleBoatSpeedFunction.java @@ -1,4 +1,4 @@ -package com.sap.sailing.dashboards.gwt.server.startlineadvantages; +package com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage; import org.apache.commons.math.ArgumentOutsideDomainException; import org.apache.commons.math.analysis.interpolation.SplineInterpolator; diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/StartlineAdvantageByGeometryCalculator.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/StartlineAdvantageByGeometryCalculator.java new file mode 100644 index 00000000000..3c0dfd8e3e9 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/StartlineAdvantageByGeometryCalculator.java @@ -0,0 +1,77 @@ +package com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage; + +import java.util.Iterator; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.tracking.GPSFix; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +/** + * @author Alexander Ries (D062114) + * + */ +@GwtIncompatible +public final class StartlineAdvantageByGeometryCalculator { + + public static Double calculateStartlineAdvantageByGeometry(TrackedRace trackedRace) { + Double result = null; + Course course = trackedRace.getRace().getCourse(); + if (course != null) { + Waypoint startlineWayPoint = course.getFirstLeg().getFrom(); + Waypoint firstmarkWayPoint = course.getFirstLeg().getTo(); + if (startlineWayPoint != null && firstmarkWayPoint != null) { + Pair startlineMarkPositions = retrieveStartlineMarkPositionsFromStartLineWayPoint(trackedRace, startlineWayPoint); + Position firstMarkPosition = retrieveFirstMarkPositionFromFirstMarkWayPoint(trackedRace, firstmarkWayPoint); + if (startlineMarkPositions != null && firstMarkPosition != null) { + Distance rcToMark = firstMarkPosition.getDistance(startlineMarkPositions.getA()); + Distance pinToMark = firstMarkPosition.getDistance(startlineMarkPositions.getB()); + double startlineadvantage = rcToMark.getMeters() - pinToMark.getMeters(); + result = new Double(startlineadvantage); + } + } + } + return result; + } + + private static Pair retrieveStartlineMarkPositionsFromStartLineWayPoint(TrackedRace trackedRace, Waypoint startLineWayPoint) { + Pair result = null; + Iterator markIterator = startLineWayPoint.getMarks().iterator(); + if (markIterator.hasNext()) { + Mark startboat = (Mark) markIterator.next(); + if (markIterator.hasNext()) { + Mark pinEnd = (Mark) markIterator.next(); + TimePoint now = MillisecondsTimePoint.now(); + Position startBoatPosition = getPositionFromMarkAtTimePoint(trackedRace, startboat, now); + Position pinEndPosition = getPositionFromMarkAtTimePoint(trackedRace, pinEnd, now); + if(startBoatPosition != null && pinEndPosition != null) { + result = new Pair(startBoatPosition, pinEndPosition); + } + } + } + return result; + } + + private static Position getPositionFromMarkAtTimePoint(TrackedRace trackedRace, Mark mark, TimePoint timePoint) { + GPSFixTrack fixTrack = trackedRace.getTrack(mark); + return fixTrack.getEstimatedPosition(timePoint, true); + } + + private static Position retrieveFirstMarkPositionFromFirstMarkWayPoint(TrackedRace trackedRace, Waypoint firstMarkWayPoint) { + Position firstMarkPosition = null; + if (firstMarkWayPoint.getMarks().iterator().hasNext()) { + Mark firstMark = firstMarkWayPoint.getMarks().iterator().next(); + TimePoint now = MillisecondsTimePoint.now(); + firstMarkPosition = getPositionFromMarkAtTimePoint(trackedRace, firstMark, now); + } + return firstMarkPosition; + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/StartlineAdvantagesCalculator.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/StartlineAdvantagesByWindCalculator.java similarity index 87% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/StartlineAdvantagesCalculator.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/StartlineAdvantagesByWindCalculator.java index a42c039702a..0b625112564 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/StartlineAdvantagesCalculator.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/StartlineAdvantagesByWindCalculator.java @@ -1,4 +1,4 @@ -package com.sap.sailing.dashboards.gwt.server.startlineadvantages; +package com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage; import java.util.ArrayList; import java.util.Collections; @@ -6,9 +6,8 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import com.sap.sailing.dashboards.gwt.server.LiveTrackedRaceListener; -import com.sap.sailing.dashboards.gwt.server.startlineadvantages.precalculation.AbstracPreCalculationDataRetriever; -import com.sap.sailing.dashboards.gwt.shared.MovingAverage; +import com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage.precalculation.AbstracPreCalculationDataRetriever; +import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; import com.sap.sailing.dashboards.gwt.shared.dto.StartLineAdvantageDTO; import com.sap.sailing.dashboards.gwt.shared.dto.StartlineAdvantagesWithMaxAndAverageDTO; import com.sap.sailing.domain.common.Bearing; @@ -17,7 +16,6 @@ import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.impl.DegreeBearingImpl; import com.sap.sailing.domain.common.impl.MeterDistance; -import com.sap.sailing.domain.polars.PolarDataService; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util.Pair; @@ -26,35 +24,22 @@ import com.sap.sse.common.Util.Pair; * @author Alexander Ries (D062114) * */ -public class StartlineAdvantagesCalculator extends AbstracPreCalculationDataRetriever implements LiveTrackedRaceListener{ +public class StartlineAdvantagesByWindCalculator extends AbstracPreCalculationDataRetriever { - private TrackedRace currentLiveTrackedRace; - private MovingAverage advantageMaximumAverage; - private PolarDataService polarDataService; + private DashboardDispatchContext dashboardDispatchContext; private DefaultPolarWindAngleBoatSpeedFunction defaultPolarSpeedWindAngleFunction; - private static final Logger logger = Logger.getLogger(StartlineAdvantagesCalculator.class.getName()); + private final Logger logger = Logger.getLogger(StartlineAdvantagesByWindCalculator.class.getName()); - public StartlineAdvantagesCalculator(PolarDataService polarDataService){ - this.advantageMaximumAverage = new MovingAverage(500); - this.polarDataService = polarDataService; + public StartlineAdvantagesByWindCalculator(DashboardDispatchContext dashboardDispatchContext){ + this.dashboardDispatchContext = dashboardDispatchContext; this.defaultPolarSpeedWindAngleFunction = new DefaultPolarWindAngleBoatSpeedFunction(); } - // Get Course Buoys - // Get Wind - // Get Maneuver Angle and Boat Speed at Angle - // Get Positions on start line with normal upwind and overlaying lay line upwind - // Calculate intersection point(s) between start line and lay lines - // Calculate duration from start line to first mark for overlaying positions - // Calculate intersection points from normal upwind start positions to first mark - // Calculate duration from two new distances for normal upwind starts - // Put all durations in one set - // Get the smallest of the durations and subtract number from every other number - public StartlineAdvantagesWithMaxAndAverageDTO getStartLineAdvantagesAccrossLineAtTimePoint(TimePoint timepoint) { + public StartlineAdvantagesWithMaxAndAverageDTO getStartLineAdvantagesAccrossLineFromTrackedRaceAtTimePoint(TrackedRace trackedRace,TimePoint timepoint) { StartlineAdvantagesWithMaxAndAverageDTO result = new StartlineAdvantagesWithMaxAndAverageDTO(); - if (currentLiveTrackedRace != null) { - retrieveDataForCalculation(currentLiveTrackedRace); + if (trackedRace != null) { + retrieveDataForCalculation(trackedRace, dashboardDispatchContext.getPolarDataService()); if (wind != null) { if (isStartlineCompletelyUnderneathLaylines()) { logger.log(Level.INFO, "Startline is completely underneath laylines"); @@ -62,8 +47,6 @@ public class StartlineAdvantagesCalculator extends AbstracPreCalculationDataRet result.advantages = calculateStartlineAdvantagesUnderneathLaylinesInRange(advantagesRange); double maximum = getMaximumAdvantageOfStartlineAdvantageDTOs(result.advantages); result.maximum = maximum; - advantageMaximumAverage.add(maximum); - result.average = advantageMaximumAverage.getAverage(); } else if (isStartlineCompletelyAboveLaylines()) { logger.log(Level.INFO, "Startline is completely above laylines"); Pair advantagesRange = new Pair(0.0, startlineLenghtInMeters); @@ -71,8 +54,6 @@ public class StartlineAdvantagesCalculator extends AbstracPreCalculationDataRet subtractMinimumOfAllStartlineAdvantages(result.advantages); double maximum = getMaximumAdvantageOfStartlineAdvantageDTOs(result.advantages); result.maximum = maximum; - advantageMaximumAverage.add(maximum); - result.average = advantageMaximumAverage.getAverage(); subtractAgainstMaximumOfAllStartlineAdvantages(result.advantages, maximum); } else { logger.log(Level.INFO, "Layline(s) cross startline"); @@ -85,8 +66,6 @@ public class StartlineAdvantagesCalculator extends AbstracPreCalculationDataRet subtractMinimumOfAllStartlineAdvantages(result.advantages); double maximum = getMaximumAdvantageOfStartlineAdvantageDTOs(result.advantages); result.maximum = maximum; - advantageMaximumAverage.add(maximum); - result.average = advantageMaximumAverage.getAverage(); subtractAgainstMaximumOfAllStartlineAdvantages(result.advantages, maximum); addClosingZeroPointToMixedAdvantages(result.advantages, pinEndStartlineAdvatagesRange); } @@ -108,7 +87,7 @@ public class StartlineAdvantagesCalculator extends AbstracPreCalculationDataRet return result; } - private boolean isBearingUnderneathAdvantageLines(Bearing bearing){ + private boolean isBearingUnderneathAdvantageLines(Bearing bearing){ boolean result = false; Bearing bearingOfRightLaylineInDeg = new DegreeBearingImpl(wind.getBearing().getDegrees() - meouvreAngle / 2); Bearing bearingOfLeftLaylineInDeg = new DegreeBearingImpl(wind.getBearing().getDegrees() + meouvreAngle / 2); @@ -314,13 +293,13 @@ public class StartlineAdvantagesCalculator extends AbstracPreCalculationDataRet return advantages; } - private double getMaximumAdvantageOfStartlineAdvantageDTOs(List advantages) { - double result = 0; + private Double getMaximumAdvantageOfStartlineAdvantageDTOs(List advantages) { + Double result = null; List sortedAdvantages = new ArrayList(); sortedAdvantages.addAll(advantages); - if(sortedAdvantages != null && sortedAdvantages.size() > 0) { - Collections.sort(sortedAdvantages, StartLineAdvantageDTO.startlineAdvantageComparatorByAdvantageDesc); - result = sortedAdvantages.get(0).startLineAdvantage; + if (sortedAdvantages != null && sortedAdvantages.size() > 0) { + Collections.sort(sortedAdvantages, StartLineAdvantageDTO.startlineAdvantageComparatorByAdvantageDesc); + result = new Double(sortedAdvantages.get(0).startLineAdvantage); } return result; } @@ -335,14 +314,4 @@ public class StartlineAdvantagesCalculator extends AbstracPreCalculationDataRet } return result; } - - @Override - public void liveTrackedRaceDidChange(TrackedRace trackedRace) { - this.currentLiveTrackedRace = trackedRace; - } - - @Override - public PolarDataService getPolarDataService() { - return this.polarDataService; - } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/AbstracPreCalculationDataRetriever.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/AbstracPreCalculationDataRetriever.java similarity index 86% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/AbstracPreCalculationDataRetriever.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/AbstracPreCalculationDataRetriever.java index 4353242c43f..1cf1ab8d0e9 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/AbstracPreCalculationDataRetriever.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/AbstracPreCalculationDataRetriever.java @@ -1,9 +1,10 @@ -package com.sap.sailing.dashboards.gwt.server.startlineadvantages.precalculation; +package com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage.precalculation; import com.sap.sailing.domain.base.impl.BoatClassImpl; import com.sap.sailing.domain.common.BoatClassMasterdata; import com.sap.sailing.domain.common.ManeuverType; import com.sap.sailing.domain.common.Wind; +import com.sap.sailing.domain.polars.PolarDataService; import com.sap.sailing.domain.tracking.TrackedRace; /** @@ -20,11 +21,11 @@ public abstract class AbstracPreCalculationDataRetriever implements PreCalculati public Wind wind; public double meouvreAngle; - protected void retrieveDataForCalculation(TrackedRace trackedRace) { + protected void retrieveDataForCalculation(TrackedRace trackedRace, PolarDataService polarDataService) { startlineAndFirstMarkPositions = retrieveMarkPositions(trackedRace); startlineAdvantageAtPinEndInMeters = retrieveStartlineAdvantage(trackedRace); startlineLenghtInMeters = retrieveStartlineLenght(trackedRace); wind = retrieveWindAtPosition(startlineAndFirstMarkPositions.startBoatPosition, trackedRace); - meouvreAngle = retrieveManouvreAngleAtWindSpeedAndBoatClass(new BoatClassImpl("Extreme40", BoatClassMasterdata.EXTREME_40), ManeuverType.TACK, wind); + meouvreAngle = retrieveManouvreAngleAtWindSpeedAndBoatClass(new BoatClassImpl("Extreme40", BoatClassMasterdata.EXTREME_40), ManeuverType.TACK, wind, polarDataService); } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationMarkRetriever.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationMarkRetriever.java similarity index 97% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationMarkRetriever.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationMarkRetriever.java index 112195ba379..ae909348bae 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationMarkRetriever.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationMarkRetriever.java @@ -1,4 +1,4 @@ -package com.sap.sailing.dashboards.gwt.server.startlineadvantages.precalculation; +package com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage.precalculation; import java.util.Iterator; diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationPolarDataRetriever.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationPolarDataRetriever.java similarity index 73% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationPolarDataRetriever.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationPolarDataRetriever.java index 2f5998945ad..c7068933782 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationPolarDataRetriever.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationPolarDataRetriever.java @@ -1,6 +1,6 @@ -package com.sap.sailing.dashboards.gwt.server.startlineadvantages.precalculation; +package com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage.precalculation; -import com.sap.sailing.dashboards.gwt.server.startlineadvantages.DefaultPolarValues; +import com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage.DefaultPolarValues; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.common.ManeuverType; import com.sap.sailing.domain.common.Speed; @@ -14,11 +14,11 @@ import com.sap.sailing.domain.polars.PolarDataService; */ public interface PreCalculationPolarDataRetriever { - default double retrieveManouvreAngleAtWindSpeedAndBoatClass(BoatClass boatClass, ManeuverType maneuverType, Speed windSpeed) { + default double retrieveManouvreAngleAtWindSpeedAndBoatClass(BoatClass boatClass, ManeuverType maneuverType, Speed windSpeed, PolarDataService polarDataService) { double result = 0; try { if (boatClass != null && maneuverType != null && windSpeed != null) { - BearingWithConfidence bearingWithConfidence = getPolarDataService().getManeuverAngle(boatClass, maneuverType, windSpeed); + BearingWithConfidence bearingWithConfidence = polarDataService.getManeuverAngle(boatClass, maneuverType, windSpeed); result = bearingWithConfidence.getObject().getDegrees(); } else { result = DefaultPolarValues.getManouvreAngle(ManeuverType.TACK); @@ -28,6 +28,4 @@ public interface PreCalculationPolarDataRetriever { } return result; } - - PolarDataService getPolarDataService(); } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationStartlineAdvantageRetriever.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationStartlineAdvantageRetriever.java similarity index 90% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationStartlineAdvantageRetriever.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationStartlineAdvantageRetriever.java index 9da4e2f2850..c02595e475d 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationStartlineAdvantageRetriever.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationStartlineAdvantageRetriever.java @@ -1,4 +1,4 @@ -package com.sap.sailing.dashboards.gwt.server.startlineadvantages.precalculation; +package com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage.precalculation; import com.sap.sailing.domain.tracking.LineDetails; import com.sap.sailing.domain.tracking.TrackedRace; diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationWindRetriever.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationWindRetriever.java similarity index 84% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationWindRetriever.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationWindRetriever.java index e52ee193d0e..f1ff3539a4f 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/startlineadvantages/precalculation/PreCalculationWindRetriever.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/precalculation/PreCalculationWindRetriever.java @@ -1,4 +1,4 @@ -package com.sap.sailing.dashboards.gwt.server.startlineadvantages.precalculation; +package com.sap.sailing.dashboards.gwt.server.util.actions.startlineadvantage.precalculation; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Wind; diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardLiveRaceProvider.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardLiveRaceProvider.java new file mode 100644 index 00000000000..a13477f7a50 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/DashboardLiveRaceProvider.java @@ -0,0 +1,55 @@ +package com.sap.sailing.dashboards.gwt.shared; + +import java.util.HashMap; +import java.util.Map; + +import com.sap.sailing.domain.base.Fleet; +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.server.RacingEventService; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +/** + * @author Alexander Ries (D062114) + * + */ +public class DashboardLiveRaceProvider { + + private Map liveRaceForLeaderboardNames; + private RacingEventService racingEventService; + + public DashboardLiveRaceProvider(RacingEventService racingEventService) { + liveRaceForLeaderboardNames = new HashMap(); + this.racingEventService = racingEventService; + } + + public void validateLiveRaceForLeaderboardName(String leaderboardName) { + Leaderboard leaderboard = racingEventService.getLeaderboardByName(leaderboardName); + TrackedRace liveRace = null; + if (leaderboard != null) { + for (RaceColumn column : leaderboard.getRaceColumns()) { + for (Fleet fleet : column.getFleets()) { + TrackedRace race = column.getTrackedRace(fleet); + if (race != null && race.isLive(MillisecondsTimePoint.now())) { + liveRace = race; + } + } + } + } + if (liveRace != null) + setLiveRaceForLeaderboardName(leaderboardName, liveRace); + } + + public TrackedRace getLiveRaceForLeaderboardName(String leaderboardName) { + return liveRaceForLeaderboardNames.get(leaderboardName); + } + + private void setLiveRaceForLeaderboardName(String leaderboardName, TrackedRace liveRace) { + if (liveRaceForLeaderboardNames.containsKey(leaderboardName)) { + liveRaceForLeaderboardNames.replace(leaderboardName, liveRace); + } else { + liveRaceForLeaderboardNames.put(leaderboardName, liveRace); + } + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/MovingAverage.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/MovingAverage.java index 84bbc5e637a..54f803eb96d 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/MovingAverage.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/MovingAverage.java @@ -5,10 +5,10 @@ package com.sap.sailing.dashboards.gwt.shared; /** - * Used to calculate the average value for a certain amount of renewing values. - * The amount of values is defined by {@link #NUMBER_OF_VALUES_AVERAGE_CALCULATED}. + * Used to calculate the average value for a defined amount of renewing values. + * The amount of values saved is defined by {@link #NUMBER_OF_VALUES_AVERAGE_CALCULATED}. * It uses a doubleEnededQueue to delete old values, when the queue reached the size - * of {@link #NUMBER_OF_FIEXES_AVERAGE_CALCULATED} and populate it with new ones. + * of {@link #NUMBER_OF_FIEXES_AVERAGE_CALCULATED} and populates it with new ones. * * @author Alexander Ries * diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/MovingAveragesCache.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/MovingAveragesCache.java new file mode 100644 index 00000000000..d04c0e8185d --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/MovingAveragesCache.java @@ -0,0 +1,44 @@ +package com.sap.sailing.dashboards.gwt.shared; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Alexander Ries (D062114) + * + */ +public class MovingAveragesCache { + + private Map averagesForStringKeys; + + public MovingAveragesCache() { + averagesForStringKeys = new HashMap(); + } + + public boolean containesCacheWithKey(String key) { + return averagesForStringKeys.containsKey(key); + } + + public void createMovingAverageWithKeyAndSize(String key, int size) { + if (!averagesForStringKeys.containsKey(key)) { + averagesForStringKeys.put(key, new MovingAverage(size)); + } + } + + public void addValueToAverageWithKey(String key, double value) { + if (averagesForStringKeys.containsKey(key)) { + MovingAverage movingAverage = averagesForStringKeys.get(key); + movingAverage.add(value); + averagesForStringKeys.replace(key, movingAverage); + } + } + + public Double getValueForKey(String key) { + Double result = null; + MovingAverage movingAverage = averagesForStringKeys.get(key); + if(movingAverage != null) { + result = new Double(movingAverage.getAverage()); + } + return result; + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardAction.java index abf992cab72..b9cc7b6ea50 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardAction.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardAction.java @@ -12,6 +12,6 @@ import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; public interface DashboardAction extends Action { @GwtIncompatible - R execute(DashboardDispatchContext ctx) throws DispatchException; + R execute(DashboardDispatchContext dashboardDispatchContext) throws DispatchException; } \ No newline at end of file diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchContext.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchContext.java index c54e21f3957..ef159975ee2 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchContext.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/DashboardDispatchContext.java @@ -4,6 +4,9 @@ import java.util.Date; import java.util.Locale; import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.dashboards.gwt.shared.DashboardLiveRaceProvider; +import com.sap.sailing.dashboards.gwt.shared.MovingAveragesCache; +import com.sap.sailing.domain.polars.PolarDataService; import com.sap.sailing.gwt.dispatch.client.DispatchContext; import com.sap.sailing.server.RacingEventService; @@ -15,6 +18,15 @@ public interface DashboardDispatchContext extends DispatchContext { @GwtIncompatible RacingEventService getRacingEventService(); + + @GwtIncompatible + PolarDataService getPolarDataService(); + + @GwtIncompatible + DashboardLiveRaceProvider getDashboardLiveRaceProvider(); + + @GwtIncompatible + MovingAveragesCache getMovingAveragesCache(); @GwtIncompatible String getClientLocaleName(); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/RequiresLiveRaceAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/RequiresLiveRaceAction.java new file mode 100644 index 00000000000..1c84c01a309 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/RequiresLiveRaceAction.java @@ -0,0 +1,30 @@ +package com.sap.sailing.dashboards.gwt.shared.dispatch; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.gwt.dispatch.client.Result; + +/** + * @author Alexander Ries (D062114) + * + */ +public abstract class RequiresLiveRaceAction implements DashboardAction{ + + private String leaderboardName; + + public RequiresLiveRaceAction() {} + + public RequiresLiveRaceAction(String leaderboardName) { + this.leaderboardName = leaderboardName; + } + + @GwtIncompatible + protected TrackedRace getLiveRace(DashboardDispatchContext dashboardDispatchContext) { + dashboardDispatchContext.getDashboardLiveRaceProvider().validateLiveRaceForLeaderboardName(this.leaderboardName); + return dashboardDispatchContext.getDashboardLiveRaceProvider().getLiveRaceForLeaderboardName(this.leaderboardName); + } + + protected String getLeaderboardName() { + return this.leaderboardName; + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/RequiresLiveRaceAndCachesMovingAverageAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/RequiresLiveRaceAndCachesMovingAverageAction.java new file mode 100644 index 00000000000..2c9907ccf49 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/RequiresLiveRaceAndCachesMovingAverageAction.java @@ -0,0 +1,30 @@ +package com.sap.sailing.dashboards.gwt.shared.dispatch; + +import com.google.gwt.core.shared.GwtIncompatible; +import com.sap.sailing.dashboards.gwt.shared.MovingAveragesCache; +import com.sap.sailing.gwt.dispatch.client.Result; + +/** + * @author Alexander Ries (D062114) + * + */ +public abstract class RequiresLiveRaceAndCachesMovingAverageAction extends RequiresLiveRaceAction{ + + private static final int MOVING_AVERAGE_CACHE_SIZE = (60/5)*15;/*15 minutes in seconds divided by request intervall*/ + + public RequiresLiveRaceAndCachesMovingAverageAction() {} + + public RequiresLiveRaceAndCachesMovingAverageAction(String leaderboardName) { + super(leaderboardName); + } + + @GwtIncompatible + protected void addValueToMovingAverage(double value, MovingAveragesCache movingAveragesCache) { + if(!movingAveragesCache.containesCacheWithKey(getKeyForMovingAverage())) { + movingAveragesCache.createMovingAverageWithKeyAndSize(getKeyForMovingAverage(), MOVING_AVERAGE_CACHE_SIZE); + } + movingAveragesCache.addValueToAverageWithKey(getKeyForMovingAverage(), value); + } + + protected abstract String getKeyForMovingAverage(); +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchContextImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchContextImpl.java index b625294cecb..706f0133c3a 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchContextImpl.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchContextImpl.java @@ -6,7 +6,10 @@ import java.util.Locale; import javax.servlet.http.HttpServletRequest; +import com.sap.sailing.dashboards.gwt.shared.DashboardLiveRaceProvider; +import com.sap.sailing.dashboards.gwt.shared.MovingAveragesCache; import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchContext; +import com.sap.sailing.domain.polars.PolarDataService; import com.sap.sailing.gwt.dispatch.client.exceptions.DispatchException; import com.sap.sailing.gwt.server.HomeServiceUtil; import com.sap.sailing.server.RacingEventService; @@ -17,14 +20,21 @@ import com.sap.sailing.server.RacingEventService; */ public class DashboardDispatchContextImpl implements DashboardDispatchContext{ private final RacingEventService racingEventService; + private final PolarDataService polarDataService; + private final DashboardLiveRaceProvider dashboardLiveRaceProvider; + private final MovingAveragesCache movingAveragesCache; private final Date currentClientTime; -// private final Date currentServerTime = new Date(); private String clientLocaleName; private final HttpServletRequest request; - public DashboardDispatchContextImpl(Date currentClientTime, RacingEventService racingEventService, String clientLocaleName, HttpServletRequest request) { + public DashboardDispatchContextImpl(Date currentClientTime, RacingEventService racingEventService, + PolarDataService polarDataService, DashboardLiveRaceProvider dashboardLiveRaceProvider, + MovingAveragesCache movingAveragesCache, String clientLocaleName, HttpServletRequest request) { this.currentClientTime = currentClientTime; this.racingEventService = racingEventService; + this.polarDataService = polarDataService; + this.dashboardLiveRaceProvider = dashboardLiveRaceProvider; + this.movingAveragesCache = movingAveragesCache; this.clientLocaleName = clientLocaleName; this.request = request; } @@ -34,6 +44,16 @@ public class DashboardDispatchContextImpl implements DashboardDispatchContext{ return racingEventService; } + @Override + public PolarDataService getPolarDataService() { + return polarDataService; + } + + @Override + public DashboardLiveRaceProvider getDashboardLiveRaceProvider() { + return dashboardLiveRaceProvider; + } + @Override public Date getCurrentClientTime() { return currentClientTime; @@ -58,5 +78,9 @@ public class DashboardDispatchContextImpl implements DashboardDispatchContext{ public URL getRequestBaseURL() throws DispatchException { return HomeServiceUtil.getRequestBaseURL(request); } - + + @Override + public MovingAveragesCache getMovingAveragesCache() { + return movingAveragesCache; + } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/AverageDTO.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/AverageDTO.java index 540ccffa8a1..df20cc4a3a5 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/AverageDTO.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/AverageDTO.java @@ -1,15 +1,15 @@ package com.sap.sailing.dashboards.gwt.shared.dto; -import com.google.gwt.user.client.rpc.IsSerializable; +import com.sap.sailing.gwt.dispatch.client.Result; /** * @author Alexander Ries (D062114) * */ -public class AverageDTO implements IsSerializable { +public class AverageDTO implements Result { - public double average; - public double collectingTimeFrameInMilliseconds; + public Double average; + public Double collectingTimeFrameInMilliseconds; public AverageDTO(){} } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/LeaderboardCompetitorsDTO.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/LeaderboardCompetitorsDTO.java new file mode 100644 index 00000000000..3becb6c12e5 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/LeaderboardCompetitorsDTO.java @@ -0,0 +1,25 @@ +package com.sap.sailing.dashboards.gwt.shared.dto; + +import java.util.List; + +import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.gwt.dispatch.client.Result; + +/** + * @author Alexander Ries (D062114) + * + */ +public class LeaderboardCompetitorsDTO implements Result { + + private List competitors; + + public LeaderboardCompetitorsDTO() {} + + public List getCompetitors() { + return competitors; + } + + public void setCompetitors(List competitors) { + this.competitors = competitors; + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/RaceIdDTO.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/RaceIdDTO.java similarity index 84% rename from java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/RaceIdDTO.java rename to java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/RaceIdDTO.java index 5d473cb8948..ee100009561 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/RaceIdDTO.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/RaceIdDTO.java @@ -1,4 +1,4 @@ -package com.sap.sailing.dashboards.gwt.shared; +package com.sap.sailing.dashboards.gwt.shared.dto; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.gwt.dispatch.client.Result; @@ -11,8 +11,7 @@ public class RaceIdDTO implements Result { private RegattaAndRaceIdentifier raceId; - public RaceIdDTO() { - } + public RaceIdDTO() {} public RegattaAndRaceIdentifier getRaceId() { return raceId; diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartAnalysesDTO.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartAnalysesDTO.java new file mode 100644 index 00000000000..4293410f977 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartAnalysesDTO.java @@ -0,0 +1,25 @@ +package com.sap.sailing.dashboards.gwt.shared.dto; + +import java.util.List; + +import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisDTO; +import com.sap.sailing.gwt.dispatch.client.Result; + +/** + * @author Alexander Ries (D062114) + * + */ +public class StartAnalysesDTO implements Result { + + private List startAnalyses; + + public StartAnalysesDTO() {} + + public List getStartAnalyses() { + return startAnalyses; + } + + public void setStartAnalyses(List startAnalyses) { + this.startAnalyses = startAnalyses; + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartLineAdvantageDTO.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartLineAdvantageDTO.java index 9d2c7fe26f2..9bddab2761f 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartLineAdvantageDTO.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartLineAdvantageDTO.java @@ -2,15 +2,15 @@ package com.sap.sailing.dashboards.gwt.shared.dto; import java.util.Comparator; -import com.google.gwt.user.client.rpc.IsSerializable; import com.sap.sailing.dashboards.gwt.client.startanalysis.StartlineAdvantageType; +import com.sap.sailing.gwt.dispatch.client.Result; -public class StartLineAdvantageDTO extends AverageDTO implements IsSerializable { +public class StartLineAdvantageDTO extends AverageDTO implements Result { - public double startLineAdvantage; - public double distanceToRCBoatInMeters; + public Double startLineAdvantage; + public Double distanceToRCBoatInMeters; public StartlineAdvantageType startLineAdvatageType; - public double confidence; + public Double confidence; public StartLineAdvantageDTO(){} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartlineAdvantagesWithMaxAndAverageDTO.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartlineAdvantagesWithMaxAndAverageDTO.java index 58c74a7973d..4dab1d3f485 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartlineAdvantagesWithMaxAndAverageDTO.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartlineAdvantagesWithMaxAndAverageDTO.java @@ -2,15 +2,15 @@ package com.sap.sailing.dashboards.gwt.shared.dto; import java.util.List; -import com.google.gwt.user.client.rpc.IsSerializable; +import com.sap.sailing.gwt.dispatch.client.Result; /** * @author Alexander Ries (D062114) * */ -public class StartlineAdvantagesWithMaxAndAverageDTO extends AverageDTO implements IsSerializable { +public class StartlineAdvantagesWithMaxAndAverageDTO extends AverageDTO implements Result { - public double maximum; + public Double maximum; public List advantages; public StartlineAdvantagesWithMaxAndAverageDTO(){} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/resources/com/sap/sailing/dashboards/gwt/RibDashboard.gwt.xml b/java/com.sap.sailing.dashboards.gwt/src/main/resources/com/sap/sailing/dashboards/gwt/RibDashboard.gwt.xml index 0c676581a97..9afdbd1ddf8 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/resources/com/sap/sailing/dashboards/gwt/RibDashboard.gwt.xml +++ b/java/com.sap.sailing.dashboards.gwt/src/main/resources/com/sap/sailing/dashboards/gwt/RibDashboard.gwt.xml @@ -43,8 +43,7 @@ - + @@ -59,5 +58,6 @@ + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/NoUI.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/NoUI.gwt.xml index ea5872a6f56..eb7cd8bbdb9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/NoUI.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/NoUI.gwt.xml @@ -26,7 +26,9 @@ - + + + @@ -51,6 +53,8 @@ + + From 997eddb7839fd0e409014b6662235a2b031af33e Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Mon, 7 Dec 2015 23:28:09 +0100 Subject: [PATCH 119/306] added javax packages to dashboard Manifest --- java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF b/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF index 3f43c949b17..7af263e912b 100644 --- a/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF @@ -25,8 +25,10 @@ Require-Bundle: com.sap.sailing.domain, com.sap.sse.gwt.adminconsole, org.apache.commons.math;bundle-version="2.1.0" Bundle-ActivationPolicy: lazy -Import-Package: com.sap.sailing.news, - com.sap.sse.util, +Import-Package: com.sap.sse.util, + javax.servlet;version="3.1.0", + javax.servlet.descriptor;version="3.1.0", + javax.servlet.http;version="3.1.0", org.osgi.framework;version="1.5.0", org.osgi.util.tracker Web-ContextPath: /dashboards From 71ad7a36ac01b2f750be1dc8896fa089bfc59e72 Mon Sep 17 00:00:00 2001 From: Generic Wiki User Date: Mon, 7 Dec 2015 23:45:33 +0000 Subject: [PATCH 120/306] use recode instead of iconv in convertELBLogToApacheFormat script --- configuration/convertELBLogToApacheFormat | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/convertELBLogToApacheFormat b/configuration/convertELBLogToApacheFormat index 4563874bf6e..2341923b4dd 100755 --- a/configuration/convertELBLogToApacheFormat +++ b/configuration/convertELBLogToApacheFormat @@ -6,7 +6,7 @@ for i in $*; do gzip -cd $i else cat $i - fi | iconv -f ISO-8859-1 -t UTF8 | + fi | recode ISO-8859-1..UTF-8 | sed -e 's/^\([^T]*\)T\([^.]*\)[^ ]* [^ ]* \([^:]*\):[^ ]* [^ ]* [^ ]* [^ ]* [^ ]* [^ ]* \([^ ]*\) \([^ ]*\) [^ ]* \(\"[^"]*\"\) \(\"[^"]*\"\).*$/'$PREFIX' \3 - - [\1:\2 +0000] \6 \4 \5 "http:\/\/'$PREFIX'" \7/' | sed -e 's/\[\([0-9]*\)-01-\([0-9]*\):/\[\2\/Jan\/\1:/' | sed -e 's/\[\([0-9]*\)-02-\([0-9]*\):/\[\2\/Feb\/\1:/' | From 55a0ad44f84095c9679f85d84f9f51e14ceb7da8 Mon Sep 17 00:00:00 2001 From: Generic Wiki User Date: Mon, 7 Dec 2015 23:45:56 +0000 Subject: [PATCH 121/306] fixed generation of per-month unique visitors reporting filenames, replacing / by - --- configuration/unique_ips_per_referrer | 6 +++--- .../unique_ips_per_referrer_generate_month_results_only | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/configuration/unique_ips_per_referrer b/configuration/unique_ips_per_referrer index 518f7f27509..612d7b77447 100755 --- a/configuration/unique_ips_per_referrer +++ b/configuration/unique_ips_per_referrer @@ -28,7 +28,7 @@ for i in $*; do gzip -cd $i else cat $i - fi | iconv -f ISO-8859-1 -t UTF8 | sed -e 's/^\([^ ]*\) \([^ ]*\) \([^ ]*\) \([^ ]*\) \[\([^:]*\):\([^]]*\)\] \"[^"]*\" [^ ]* [^ ]* \"[^"]*\" \"\([^"]*\)\"/\1 \2 \5 \7/' | while read referrer hit; do + fi | recode ISO-8859-1..UTF-8 | sed -e 's/^\([^ ]*\) \([^ ]*\) \([^ ]*\) \([^ ]*\) \[\([^:]*\):\([^]]*\)\] \"[^"]*\" [^ ]* [^ ]* \"[^"]*\" \"\([^"]*\)\"/\1 \2 \5 \7/' | while read referrer hit; do echo "$hit" >>${STATS}/${referrer}.ips done echo "$i" >>$VISITED_FILES @@ -49,7 +49,7 @@ for month in `cat $MONTHS`; do echo -n "`basename $i .ips` " cat $i | grep "^[^ ]* [0-9]*/$month .*" | sort -u | wc | awk '{ print $1; }' # TODO consider producing an AWStats configuration file for each virtual host name and splitting all new logs into virtual host name-specific log files - done | tee $STATS/unique-ips-days-useragents-per-event-$month - echo "Wrote per-month results to $STATS/unique-ips-days-useragents-per-event-$month" + done | tee $STATS/unique-ips-days-useragents-per-event-`echo $month | tr / -` + echo "Wrote per-month results to $STATS/unique-ips-days-useragents-per-event-`echo $month | tr / -`" done echo "Done at `date`." diff --git a/configuration/unique_ips_per_referrer_generate_month_results_only b/configuration/unique_ips_per_referrer_generate_month_results_only index dcea8f54211..d12bddaa7ae 100755 --- a/configuration/unique_ips_per_referrer_generate_month_results_only +++ b/configuration/unique_ips_per_referrer_generate_month_results_only @@ -11,6 +11,6 @@ for month in `cat $MONTHS`; do cat $i | grep "^[^ ]* [0-9]*/$month .*" | sort -u | wc | awk '{ print $1; }' # TODO consider producing an AWStats configuration file for each virtual host name and splitting all new logs into virtual host name-specific log files done | tee $STATS/unique-ips-days-useragents-per-event-`echo $month | tr / -` - echo "Wrote per-month results to $STATS/unique-ips-days-useragents-per-event-$month" + echo "Wrote per-month results to $STATS/unique-ips-days-useragents-per-event-`echo $month | tr / -`" done echo "Done." From a5a7b3d3d4446f1ef692fe4e929e433c6acb9a56 Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Tue, 8 Dec 2015 09:05:34 +0100 Subject: [PATCH 122/306] replace blurring effect for popup with simple more performant darkening --- .../WEB-INF/web.xml | 12 -- .../gwt/client/DashboardClientFactory.java | 5 + .../client/DashboardClientFactoryImpl.java | 11 ++ .../gwt/client/RibDashboardService.java | 26 ----- .../gwt/client/RibDashboardServiceAsync.java | 26 ----- .../GetRibDashboardRaceInfoAction.java | 22 ---- .../RibDashboardDataRetriever.java | 110 ------------------ .../RibDashboardDataRetrieverListener.java | 16 --- .../CompetitorSelectionPopup.css | 12 +- .../CompetitorSelectionPopup.java | 23 ++-- .../CompetitorSelectionPopup.ui.xml | 29 ++--- .../gwt/client/visualeffects/BlurEffect.css | 8 +- .../client/windchart/WindBotComponent.java | 1 - 13 files changed, 59 insertions(+), 242 deletions(-) delete mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardService.java delete mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardServiceAsync.java delete mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetRibDashboardRaceInfoAction.java delete mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java delete mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetrieverListener.java diff --git a/java/com.sap.sailing.dashboards.gwt/WEB-INF/web.xml b/java/com.sap.sailing.dashboards.gwt/WEB-INF/web.xml index 2181d54550d..c6c14b60dbb 100644 --- a/java/com.sap.sailing.dashboards.gwt/WEB-INF/web.xml +++ b/java/com.sap.sailing.dashboards.gwt/WEB-INF/web.xml @@ -18,18 +18,6 @@ - - Rib Dashboard Service - RibDashboardService - com.sap.sailing.dashboards.gwt.server.RibDashboardServiceImpl - - - - RibDashboardService - /service/ribdashboard - - - Sailing Service SailingService diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java index d667d6bb2ec..5475b7f583b 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactory.java @@ -2,12 +2,17 @@ package com.sap.sailing.dashboards.gwt.client; import com.sap.sailing.dashboards.gwt.shared.dispatch.DashboardDispatchSystem; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; +import com.sap.sse.gwt.client.player.Timer; /** * @author Alexander Ries (D062114) * */ public interface DashboardClientFactory{ + DashboardDispatchSystem getDispatch(); + SailingServiceAsync getSailingService(); + + Timer getDashboardFiveSecondsTimer(); } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java index afaacf3799b..7174a13fecc 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java @@ -7,6 +7,8 @@ import com.sap.sailing.dashboards.gwt.shared.dispatch.impl.DashboardDispatchSyst import com.sap.sailing.gwt.ui.client.SailingService; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sse.gwt.client.EntryPointHelper; +import com.sap.sse.gwt.client.player.Timer; +import com.sap.sse.gwt.client.player.Timer.PlayModes; /** * @author Alexander Ries (D062114) @@ -16,12 +18,16 @@ public class DashboardClientFactoryImpl implements DashboardClientFactory{ private final DashboardDispatchSystem dispatch; private final SailingServiceAsync sailingService; + private final Timer dashboardFiveSecondsTimer; + private static final int DASHBOARD_RERFRESH_INTERVAL = 5000; public DashboardClientFactoryImpl() { dispatch = new DashboardDispatchSystemImpl(); sailingService = GWT.create(SailingService.class); EntryPointHelper.registerASyncService((ServiceDefTarget) sailingService, RemoteServiceMappingConstants.WEB_CONTEXT_PATH, RemoteServiceMappingConstants.sailingServiceRemotePath); + dashboardFiveSecondsTimer = new Timer(PlayModes.Live); + dashboardFiveSecondsTimer.setRefreshInterval(DASHBOARD_RERFRESH_INTERVAL); } @Override @@ -34,4 +40,9 @@ public class DashboardClientFactoryImpl implements DashboardClientFactory{ return sailingService; } + @Override + public Timer getDashboardFiveSecondsTimer() { + return dashboardFiveSecondsTimer; + } + } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardService.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardService.java deleted file mode 100644 index 92bd68a8263..00000000000 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardService.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sap.sailing.dashboards.gwt.client; - -import java.util.List; - -import com.google.gwt.user.client.rpc.RemoteService; -import com.sap.sailing.dashboards.gwt.shared.dto.RibDashboardRaceInfoDTO; -import com.sap.sailing.dashboards.gwt.shared.dto.StartlineAdvantagesWithMaxAndAverageDTO; -import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisDTO; -import com.sap.sailing.domain.common.NoWindException; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.dto.CompetitorDTO; - -public interface RibDashboardService extends RemoteService { - - RegattaAndRaceIdentifier getIDFromRaceThatIsLive(String leaderboardName); - - RibDashboardRaceInfoDTO getLiveRaceInfo(String leaderboardName) throws NoWindException; - - RegattaAndRaceIdentifier getIDFromRaceThatTakesWindFixesNow(String leaderboardName); - - List getStartAnalysisListForCompetitorIDAndLeaderboardName(String competitorIdAsString, String leaderboardName); - - List getCompetitorsInLeaderboard(String leaderboardName); - - StartlineAdvantagesWithMaxAndAverageDTO getAdvantagesOnStartline(String leaderboardName); -} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardServiceAsync.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardServiceAsync.java deleted file mode 100644 index d2def0e6f24..00000000000 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardServiceAsync.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.sap.sailing.dashboards.gwt.client; - -import java.util.List; - -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.sap.sailing.dashboards.gwt.shared.dto.RibDashboardRaceInfoDTO; -import com.sap.sailing.dashboards.gwt.shared.dto.StartlineAdvantagesWithMaxAndAverageDTO; -import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisDTO; -import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; -import com.sap.sailing.domain.common.dto.CompetitorDTO; - -public interface RibDashboardServiceAsync { - - void getIDFromRaceThatIsLive(String leaderboardName, AsyncCallback callback); - - void getLiveRaceInfo(String leaderboardName, AsyncCallback callback); - - void getStartAnalysisListForCompetitorIDAndLeaderboardName(String competitorIdAsString, String leaderboardName, AsyncCallback> callback); - - void getCompetitorsInLeaderboard(String leaderboardName, AsyncCallback> callback); - - void getAdvantagesOnStartline(String leaderboardName, AsyncCallback callback); - - void getIDFromRaceThatTakesWindFixesNow(String leaderboardName, AsyncCallback callback); - -} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetRibDashboardRaceInfoAction.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetRibDashboardRaceInfoAction.java deleted file mode 100644 index 78fda2421be..00000000000 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/actions/GetRibDashboardRaceInfoAction.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.sap.sailing.dashboards.gwt.client.actions; - -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; -import com.sap.sailing.dashboards.gwt.shared.dto.RibDashboardRaceInfoDTO; -import com.sap.sse.gwt.client.async.AsyncAction; - -public class GetRibDashboardRaceInfoAction implements AsyncAction { - - private final RibDashboardServiceAsync ribDashboardService; - private final String leaderboardName; - - public GetRibDashboardRaceInfoAction(RibDashboardServiceAsync ribDashboardService, String leaderboardName) { - this.ribDashboardService = ribDashboardService; - this.leaderboardName = leaderboardName; - } - - @Override - public void execute(AsyncCallback callback) { - ribDashboardService.getLiveRaceInfo(leaderboardName, callback); - } -} \ No newline at end of file diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java deleted file mode 100644 index 0c54183bd04..00000000000 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetriever.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.sap.sailing.dashboards.gwt.client.dataretriever; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -import com.google.gwt.user.client.rpc.AsyncCallback; -import com.sap.sailing.dashboards.gwt.client.RibDashboardServiceAsync; -import com.sap.sailing.dashboards.gwt.client.actions.GetRibDashboardRaceInfoAction; -import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; -import com.sap.sailing.dashboards.gwt.shared.dto.RibDashboardRaceInfoDTO; -import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.TimeListener; - -/** - * Class calls method {@link #requestLiveRaceInfoFromRibDashboadService()) to retrieve new - * data for the dashboard widgets on a regularly basis. - * The request interval is 5 seconds. - * - * @author Alexander Ries - * - */ -public class RibDashboardDataRetriever implements TimeListener { - - private ArrayList dataRetrieverListener; - - private AsyncActionsExecutor asyncActionsExecutor; - - private final RibDashboardServiceAsync ribDashboardService; - - private final Object MUTEX; - - private static final Logger logger = Logger.getLogger(RibDashboardDataRetriever.class.getName()); - - public RibDashboardDataRetriever(RibDashboardServiceAsync ribDashboardService) { - initNonFinalMemberVariablesWithNoArgumentConstructor(); - MUTEX = new Object(); - this.ribDashboardService = ribDashboardService; - asyncActionsExecutor = new AsyncActionsExecutor(); - } - - private void initNonFinalMemberVariablesWithNoArgumentConstructor() { - dataRetrieverListener = new ArrayList(); - } - - private void loadLiveRaceInfoFromRibDashboadService() { - - GetRibDashboardRaceInfoAction getRibDashboardRaceInfoAction = new GetRibDashboardRaceInfoAction(ribDashboardService, DashboardURLParameters.LEADERBOARD_NAME.getValue()); - logger.log(Level.INFO, "Executing GetRibDashboardRaceInfoAction"); - asyncActionsExecutor.execute(getRibDashboardRaceInfoAction, new AsyncCallback() { - @Override - public void onSuccess(RibDashboardRaceInfoDTO result) { - logger.log(Level.INFO, "Received RibDashboardRaceInfoDTO"); - if (result != null) { - switch (result.responseMessage) { - case RACE_LIVE: - logger.log(Level.INFO, "RibDashboardRaceInfoDTO.responseMessage is RACE_LIVE"); - notifyDataObservers(result); - break; - case NO_RACE_LIVE: - logger.log(Level.INFO, "RibDashboardRaceInfoDTO.responseMessage is NO_RACE_LIVE"); - break; - default: - logger.log(Level.INFO, "RibDashboardRaceInfoDTO.responseMessage is null"); - break; - } - } else { - logger.log(Level.INFO, "RibDashboardRaceInfoDTO is null"); - } - } - - @Override - public void onFailure(Throwable caught) { - logger.log(Level.INFO, "Failed to received RibDashboardRaceInfoDTO, "+caught.getMessage()); - } - }); - } - - public void addDataObserver(RibDashboardDataRetrieverListener o) { - synchronized (MUTEX) { - if (o != null) { - dataRetrieverListener.add(o); - } - } - } - - public void removeDataObserver(RibDashboardDataRetrieverListener o) { - synchronized (MUTEX) { - this.dataRetrieverListener.remove(o); - } - } - - public void notifyDataObservers(RibDashboardRaceInfoDTO liveRaceInfoDTO) { - logger.log(Level.INFO, "Notifing RibDashboardDataRetrieverListener about new RibDashboardRaceInfoDTO"); - List dataObserverCopy; - synchronized (MUTEX) { - dataObserverCopy = new ArrayList(dataRetrieverListener); - } - for (RibDashboardDataRetrieverListener o : dataObserverCopy) { - o.updateUIWithNewLiveRaceInfo(liveRaceInfoDTO); - } - } - - @Override - public void timeChanged(Date newTime, Date oldTime) { - loadLiveRaceInfoFromRibDashboadService(); - } -} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetrieverListener.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetrieverListener.java deleted file mode 100644 index ee2e95944c1..00000000000 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/RibDashboardDataRetrieverListener.java +++ /dev/null @@ -1,16 +0,0 @@ -/** - * - */ -package com.sap.sailing.dashboards.gwt.client.dataretriever; - -import com.sap.sailing.dashboards.gwt.shared.dto.RibDashboardRaceInfoDTO; - -/** - * - * @author Alexander Ries - * - */ -public interface RibDashboardDataRetrieverListener { - - public void updateUIWithNewLiveRaceInfo(RibDashboardRaceInfoDTO liveRaceInfoDTO); -} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.css b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.css index a2ba055f0f5..665596c0f8d 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.css +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.css @@ -1,5 +1,13 @@ @CHARSET "UTF-8"; +.competitorselectionbackground { + position: absolute; + background-color: rgba(0, 0, 0, 0.6); + width: 100%; + height: 100%; + z-index: 1; +} + .competitorselectionpopup { position: absolute; left: 50%; @@ -8,7 +16,7 @@ height: 200pt; margin-top: -600pt; margin-left: -100pt; - background-color: white; + background-color: rgba(255, 255, 255, 1); border-radius: 0px; z-index: 2; } @@ -23,7 +31,7 @@ vertical-align: middle; text-align: center; font-weight: bold; - text-shadow: 0 1px 1px rgba(255, 255, 255, .7); + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.7); width: 100%; z-index: 3; } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.java index 0adc18b350c..3193a59b6c1 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.java @@ -15,9 +15,8 @@ import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.RootLayoutPanel; -import com.google.gwt.user.client.ui.RootPanel; +import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.dashboards.gwt.client.visualeffects.BlurEffect; import com.sap.sailing.domain.common.dto.CompetitorDTO; public class CompetitorSelectionPopup extends Composite implements HasWidgets, CompetitorTableRowSelectionListener { @@ -33,6 +32,12 @@ public class CompetitorSelectionPopup extends Composite implements HasWidgets, C @UiField CompetitorSelectionPopupStyle style; + @UiField + HTMLPanel competitorselectionbackground; + + @UiField + VerticalPanel competitorselectionpopup; + @UiField HTMLPanel header; @@ -61,19 +66,17 @@ public class CompetitorSelectionPopup extends Composite implements HasWidgets, C public void show(List competitorList) { competitortable.setTableContent(competitorList); - RootPanel.get().add(this); + RootLayoutPanel.get().add(this); this.button.setText("OK"); - this.getElement().addClassName(style.popupshow()); - this.getElement().removeClassName(style.popuphide()); - BlurEffect.getInstance().addToView(RootLayoutPanel.get()); + competitorselectionpopup.addStyleName(style.popupshow()); + competitorselectionpopup.removeStyleName(style.popuphide()); isVisible = true; } public void hide() { - this.getElement().addClassName(style.popuphide()); - this.getElement().removeClassName(style.popupshow()); - BlurEffect.getInstance().removeFromView(RootLayoutPanel.get()); - RootPanel.get().remove(this); + competitorselectionpopup.addStyleName(style.popuphide()); + competitorselectionpopup.removeStyleName(style.popupshow()); + RootLayoutPanel.get().remove(this); isVisible = false; } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.ui.xml b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.ui.xml index bb2812d51ed..9c762ce1e0e 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.ui.xml +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/popups/competitorselection/CompetitorSelectionPopup.ui.xml @@ -1,22 +1,25 @@ - - - Select your Team - - - - - - - + + + Select your Team + + + + + + + + \ No newline at end of file diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/visualeffects/BlurEffect.css b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/visualeffects/BlurEffect.css index 6d533471683..7e1020ab331 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/visualeffects/BlurEffect.css +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/visualeffects/BlurEffect.css @@ -1,11 +1,11 @@ @CHARSET "UTF-8"; .blurred { - -webkit-filter: blur(10pt) brightness(0.92); - filter: blur(10pt) brightness(0.92); + -webkit-filter: brightness(0.7); + filter: brightness(0.7); } .not_blurred { - -webkit-filter: blur(0pt) brightness(1); - filter: blur(0pt) brightness(1); + -webkit-filter: brightness(1); + filter: brightness(1); } \ No newline at end of file diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/WindBotComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/WindBotComponent.java index acba0c39e09..04b3062b75e 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/WindBotComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/WindBotComponent.java @@ -17,7 +17,6 @@ import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.dashboards.gwt.client.dataretriever.RibDashboardDataRetrieverListener; import com.sap.sailing.dashboards.gwt.client.dataretriever.WindBotDataRetrieverListener; import com.sap.sailing.dashboards.gwt.client.startlineadvantage.LiveAverageComponent; import com.sap.sailing.dashboards.gwt.client.windchart.compass.LocationPointerCompass; From c22a2709e93d0622a4f9e14f9597e2ad6ec59809 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Tue, 8 Dec 2015 10:19:33 +0100 Subject: [PATCH 123/306] newSelectionModel: refactored SelectionCheckboxColumn to avoid Stackoverflows in combinatin with the RefreshableMultiSelectionModel --- .../AbstractLeaderboardConfigPanel.java | 30 ++++++++--- .../AbstractTrackedRacesListComposite.java | 8 +-- .../ui/adminconsole/EventListComposite.java | 5 +- .../adminconsole/LeaderboardConfigPanel.java | 9 +++- .../LeaderboardGroupConfigPanel.java | 26 +++++++--- .../ui/adminconsole/RegattaListComposite.java | 5 +- ...martphoneTrackingEventManagementPanel.java | 8 ++- .../StructureImportListComposite.java | 5 +- .../gwt/ui/adminconsole/TableWrapper.java | 9 ++-- .../TracTracEventManagementPanel.java | 8 +-- ...bleSortedCellTableWithStylableHeaders.java | 11 ++++ .../controls/SelectionCheckboxColumn.java | 50 ++++++++----------- .../gwt/ui/leaderboard/LeaderboardPanel.java | 30 +++++++---- 13 files changed, 128 insertions(+), 76 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableSortedCellTableWithStylableHeaders.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index fc3ffe3b8b6..19ca712de3c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -40,6 +40,7 @@ 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.shared.RaceLogDTO; import com.sap.sailing.gwt.ui.shared.RegattaDTO; @@ -48,6 +49,7 @@ import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; 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.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; @@ -68,7 +70,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement protected final ErrorReporter errorReporter; - protected final CellTable leaderboardTable; + protected final FlushableCellTable leaderboardTable; protected final RaceTableWrapper> raceColumnTable; protected final RefreshableSelectionModel raceColumnTableSelectionModel; @@ -159,7 +161,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement leaderboardsPanel.add(leaderboardControlsPanel); AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); - leaderboardTable = new CellTable(/* pageSize */10000, tableRes); + leaderboardTable = new FlushableCellTable(/* pageSize */10000, tableRes); filterLeaderboardPanel = new LabeledAbstractFilterablePanel(lblFilterEvents, availableLeaderboardList, leaderboardTable, leaderboardList) { @Override @@ -174,7 +176,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement leaderboardsPanel.add(filterLeaderboardPanel); leaderboardTable.ensureDebugId("AvailableLeaderboardsTable"); - addColumnsToLeaderboardTableAndSetSelectionModel(leaderboardTable, tableRes); + addColumnsToLeaderboardTableAndSetSelectionModel(leaderboardTable, tableRes, leaderboardList); @SuppressWarnings("unchecked") RefreshableMultiSelectionModel refreshableMultiSelectionModel = (RefreshableMultiSelectionModel) leaderboardTable.getSelectionModel(); refreshableLeaderboardSelectionModel = refreshableMultiSelectionModel; @@ -283,15 +285,27 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement } protected abstract void addLeaderboardControls(Panel controlsPanel); + protected abstract void addSelectedLeaderboardRacesControls(Panel racesPanel); - protected abstract void addColumnsToLeaderboardTableAndSetSelectionModel(CellTable leaderboardTable, AdminConsoleTableResources tableRes); + + protected abstract void addColumnsToLeaderboardTableAndSetSelectionModel( + FlushableCellTable leaderboardTable, AdminConsoleTableResources tableRes, + ListDataProvider leaderboardListDataProvider); protected abstract void addColumnsToRacesTable(CellTable racesTable); protected SelectionCheckboxColumn createSortableSelectionCheckboxColumn( - final CellTable leaderboardTable, AdminConsoleTableResources tableResources, - ListHandler leaderboardColumnListHandler) { - SelectionCheckboxColumn selectionCheckboxColumn = new SelectionCheckboxColumn(tableResources.cellTableStyle().cellTableCheckboxSelected(), - tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { + final FlushableCellTable leaderboardTable, AdminConsoleTableResources tableResources, + ListHandler leaderboardColumnListHandler, ListDataProvider listDataProvider) { + SelectionCheckboxColumn selectionCheckboxColumn = new SelectionCheckboxColumn( + tableResources.cellTableStyle().cellTableCheckboxSelected(), + tableResources.cellTableStyle().cellTableCheckboxDeselected(), + tableResources.cellTableStyle().cellTableCheckboxColumnCell(), + new EntityIdentityComparator() { + @Override + public boolean representSameEntity(StrippedLeaderboardDTO dto1, StrippedLeaderboardDTO dto2) { + return dto1.name.equals(dto2.name); + } + }, listDataProvider, leaderboardTable) { @Override protected ListDataProvider getListDataProvider() { return leaderboardList; 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 d1e51d56445..413ca5ead14 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 @@ -12,7 +12,6 @@ import com.google.gwt.event.dom.client.ClickHandler; 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.CellTable; 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; @@ -35,6 +34,7 @@ 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; @@ -60,7 +60,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl private SelectionCheckboxColumn selectionCheckboxColumn; - private CellTable raceTable; + private FlushableCellTable raceTable; private ListDataProvider raceList; @@ -114,7 +114,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl noTrackedRacesLabel.setWordWrap(false); panel.add(noTrackedRacesLabel); AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); - raceTable = new CellTable(/* pageSize */10000, tableRes); + raceTable = new FlushableCellTable(/* pageSize */10000, tableRes); raceTable.ensureDebugId("TrackedRacesCellTable"); final EntityIdentityComparator entityIdentityComparator = new EntityIdentityComparator() { @Override @@ -127,7 +127,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), - entityIdentityComparator) { + entityIdentityComparator, raceList, raceTable) { @Override protected ListDataProvider getListDataProvider() { return raceList; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 014e1d5dfc8..414037d64cf 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -37,6 +37,7 @@ import com.sap.sailing.gwt.ui.client.EventsRefresher; import com.sap.sailing.gwt.ui.client.LeaderboardGroupsDisplayer; 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.CourseAreaDTO; @@ -181,7 +182,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le } private CellTable createEventTable() { - CellTable table = new CellTable(/* pageSize */10000, tableRes); + FlushableCellTable table = new FlushableCellTable(/* pageSize */10000, tableRes); eventListDataProvider.addDataDisplay(table); table.setWidth("100%"); @@ -193,7 +194,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le public boolean representSameEntity(EventDTO dto1, EventDTO dto2) { return dto1.id.equals(dto2.id); } - }) { + },eventListDataProvider,table) { @Override protected ListDataProvider getListDataProvider() { return eventListDataProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java index e102d81bb44..898eef6a3a4 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java @@ -28,6 +28,7 @@ import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.HasHorizontalAlignment; import com.google.gwt.user.client.ui.Panel; +import com.google.gwt.view.client.ListDataProvider; import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.RegattaName; import com.sap.sailing.domain.common.dto.AbstractLeaderboardDTO; @@ -42,6 +43,7 @@ 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.leaderboard.LeaderboardEntryPoint; import com.sap.sailing.gwt.ui.leaderboard.ScoringSchemeTypeFormatter; @@ -124,11 +126,14 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { } @Override - protected void addColumnsToLeaderboardTableAndSetSelectionModel(final CellTable leaderboardTable, AdminConsoleTableResources tableResources) { + protected void addColumnsToLeaderboardTableAndSetSelectionModel( + final FlushableCellTable leaderboardTable, + AdminConsoleTableResources tableResources, + ListDataProvider leaderboardListDataProvider) { ListHandler leaderboardColumnListHandler = new ListHandler( leaderboardList.getList()); SelectionCheckboxColumn selectionCheckboxColumn = createSortableSelectionCheckboxColumn( - leaderboardTable, tableResources, leaderboardColumnListHandler); + leaderboardTable, tableResources, leaderboardColumnListHandler, leaderboardListDataProvider); AnchorCell anchorCell = new AnchorCell(); Column linkColumn = new Column(anchorCell) { @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java index b1b72cd85d4..a8029ec374f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java @@ -42,6 +42,7 @@ import com.sap.sailing.gwt.ui.client.LeaderboardsRefresher; import com.sap.sailing.gwt.ui.client.RegattaRefresher; 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.leaderboard.ScoringSchemeTypeFormatter; import com.sap.sailing.gwt.ui.raceboard.RaceBoardViewConfiguration; @@ -83,7 +84,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements private boolean isSingleGroupSelected; private ListDataProvider groupsProvider; - private CellTable groupDetailsTable; + private FlushableCellTable groupDetailsTable; private RefreshableMultiSelectionModel refreshableGroupDetailsSelectionModel; private ListDataProvider groupDetailsProvider; private Button editDescriptionButton; @@ -94,7 +95,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements private Button leaderboardDownButton; private LabeledAbstractFilterablePanel leaderboardsFilterablePanel; - private CellTable leaderboardsTable; + private FlushableCellTable leaderboardsTable; private RefreshableMultiSelectionModel refreshableLeaderboardsSelectionModel; private ListDataProvider leaderboardsProvider; @@ -188,14 +189,20 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements // Create leaderboards table leaderboardsProvider = new ListDataProvider(); ListHandler leaderboardsListHandler = new ListHandler(leaderboardsProvider.getList()); - leaderboardsTable = new CellTable(10000, tableRes); + leaderboardsTable = new FlushableCellTable(10000, tableRes); leaderboardsTable.ensureDebugId("LeaderboardsCellTable"); SelectionCheckboxColumn leaderboardTableSelectionColumn = new SelectionCheckboxColumn( tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), - null /* entityIdentityComparator to create a RefreshableSelectionModel */) { + new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(StrippedLeaderboardDTO dto1, StrippedLeaderboardDTO dto2) { + return dto1.name.equals(dto2.name); + } + }, leaderboardsProvider, leaderboardsTable) { @Override protected ListDataProvider getListDataProvider() { return leaderboardsProvider; @@ -362,13 +369,20 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements } }; - groupDetailsTable = new CellTable(10000, tableRes); + groupDetailsTable = new FlushableCellTable(10000, tableRes); groupDetailsTable.ensureDebugId("LeaderboardGroupsCellTable"); groupDetailsProvider = new ListDataProvider(); groupDetailsProvider.addDataDisplay(groupDetailsTable); SelectionCheckboxColumn groupDetailsTableSelectionColumn = new SelectionCheckboxColumn( - tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/) { + tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { + + @Override + public boolean representSameEntity(StrippedLeaderboardDTO dto1, + StrippedLeaderboardDTO dto2) { + return dto1.name.equals(dto2.name); + } + }, groupDetailsProvider, groupDetailsTable) { @Override protected ListDataProvider getListDataProvider() { return groupDetailsProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index 95e75efe996..ecc94aaa8a8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -30,6 +30,7 @@ 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.leaderboard.RankingMetricTypeFormatter; @@ -125,7 +126,7 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer } protected CellTable createRegattaTable() { - CellTable table = new CellTable(/* pageSize */10000, tableRes); + FlushableCellTable table = new FlushableCellTable(/* pageSize */10000, tableRes); regattaListDataProvider.addDataDisplay(table); table.setWidth("100%"); @@ -137,7 +138,7 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer public boolean representSameEntity(RegattaDTO dto1, RegattaDTO dto2) { return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); } - }) { + },regattaListDataProvider, table) { @Override protected ListDataProvider getListDataProvider() { return regattaListDataProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java index f4a96bd60e0..d0df5db278e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java @@ -19,6 +19,7 @@ import com.google.gwt.user.client.ui.CaptionPanel; import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.VerticalPanel; +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.PassingInstruction; @@ -31,6 +32,7 @@ import com.sap.sailing.gwt.ui.client.LeaderboardsRefresher; import com.sap.sailing.gwt.ui.client.RegattaRefresher; 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.shared.ControlPointDTO; import com.sap.sailing.gwt.ui.shared.EventDTO; @@ -84,11 +86,13 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC } @Override - protected void addColumnsToLeaderboardTableAndSetSelectionModel(CellTable leaderboardTable, AdminConsoleTableResources tableResources) { + protected void addColumnsToLeaderboardTableAndSetSelectionModel(FlushableCellTable leaderboardTable, + AdminConsoleTableResources tableResources, + ListDataProvider leaderboardListDataProvider) { ListHandler leaderboardColumnListHandler = new ListHandler( leaderboardList.getList()); SelectionCheckboxColumn selectionCheckboxColumn = createSortableSelectionCheckboxColumn( - leaderboardTable, tableResources, leaderboardColumnListHandler); + leaderboardTable, tableResources, leaderboardColumnListHandler, leaderboardListDataProvider); TextColumn leaderboardNameColumn = new TextColumn() { @Override public String getValue(StrippedLeaderboardDTO leaderboard) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java index afdd99e7825..e50fcc1cdae 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java @@ -10,6 +10,7 @@ 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.shared.RegattaDTO; import com.sap.sse.common.util.NaturalComparator; @@ -35,7 +36,7 @@ public class StructureImportListComposite extends RegattaListComposite implement // create Regatta Table in StructureImportManagementPanel @Override protected CellTable createRegattaTable() { - CellTable table = new CellTable(/* pageSize */10000, tableRes); + FlushableCellTable table = new FlushableCellTable(/* pageSize */10000, tableRes); regattaListDataProvider.addDataDisplay(table); table.setWidth("100%"); @@ -47,7 +48,7 @@ public class StructureImportListComposite extends RegattaListComposite implement public boolean representSameEntity(RegattaDTO dto1, RegattaDTO dto2) { return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); } - }) { + }, regattaListDataProvider, table) { @Override protected ListDataProvider getListDataProvider() { return regattaListDataProvider; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java index 5edfb750251..b0c2ac85ade 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java @@ -10,6 +10,7 @@ import com.google.gwt.user.client.ui.Widget; import com.google.gwt.view.client.ListDataProvider; 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.sse.common.Util; import com.sap.sse.gwt.client.ErrorReporter; @@ -23,7 +24,7 @@ import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; * created by this class's constructor by calling {@link #getColumnSortHandler}. */ public abstract class TableWrapper> implements IsWidget { - protected final CellTable table; + protected final FlushableCellTable table; private final S selectionModel; protected final ListDataProvider dataProvider; protected VerticalPanel mainPanel; @@ -44,7 +45,7 @@ public abstract class TableWrapper> im this.entityIdentityComparator = entityIdentityComparator; this.sailingService = sailingService; this.errorReporter = errorReporter; - table = new CellTable(10000, tableRes); + table = new FlushableCellTable(10000, tableRes); this.dataProvider = new ListDataProvider(); this.columnSortHandler = new ListHandler(dataProvider.getList()); table.addColumnSortHandler(this.columnSortHandler); @@ -52,7 +53,7 @@ public abstract class TableWrapper> im SelectionCheckboxColumn selectionCheckboxColumn = new SelectionCheckboxColumn( tableRes.cellTableStyle().cellTableCheckboxSelected(), tableRes.cellTableStyle().cellTableCheckboxDeselected(), - tableRes.cellTableStyle().cellTableCheckboxColumnCell(), this.entityIdentityComparator) { + tableRes.cellTableStyle().cellTableCheckboxColumnCell(), this.entityIdentityComparator, dataProvider, table) { @Override protected ListDataProvider getListDataProvider() { return dataProvider; @@ -66,7 +67,7 @@ public abstract class TableWrapper> im table.addColumn(selectionCheckboxColumn, selectionCheckboxColumn.getHeader()); } else { @SuppressWarnings("unchecked") - S typedSelectionModel = (S) new RefreshableSingleSelectionModel(entityIdentityComparator); + S typedSelectionModel = (S) new RefreshableSingleSelectionModel(entityIdentityComparator, dataProvider); selectionModel = typedSelectionModel; table.setSelectionModel(selectionModel); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java index c47a621750b..c498cb1640c 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java @@ -14,7 +14,6 @@ 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.user.cellview.client.CellTable; 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; @@ -40,6 +39,7 @@ import com.sap.sailing.domain.common.RegattaName; import com.sap.sailing.gwt.ui.client.RegattaRefresher; 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.shared.RegattaDTO; import com.sap.sailing.gwt.ui.shared.TracTracConfigurationDTO; @@ -84,7 +84,7 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { private Label loadingMessageLabel; private LabeledAbstractFilterablePanel racesFilterablePanel; - private CellTable racesTable; + private FlushableCellTable racesTable; private static final String ZERO_AS_STRING = "0"; public TracTracEventManagementPanel(final SailingServiceAsync sailingService, ErrorReporter errorReporter, @@ -314,7 +314,7 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { // Filter Label racesFilterLabel = new Label(stringMessages.filterRacesByName() + ":"); AdminConsoleTableResources tableResources = GWT.create(AdminConsoleTableResources.class); - racesTable = new CellTable(10000, tableResources); + racesTable = new FlushableCellTable(10000, tableResources); racesTable.ensureDebugId("TrackableRacesCellTable"); this.racesFilterablePanel = new LabeledAbstractFilterablePanel(racesFilterLabel, availableTracTracRaces, racesTable, raceList) { @Override @@ -388,7 +388,7 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { public boolean representSameEntity(TracTracRaceRecordDTO dto1, TracTracRaceRecordDTO dto2) { return dto1.id.equals(dto2.id); } - }) { + }, raceList, racesTable) { @Override public Boolean getValue(TracTracRaceRecordDTO row) { return racesTable.getSelectionModel().isSelected(row); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableSortedCellTableWithStylableHeaders.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableSortedCellTableWithStylableHeaders.java new file mode 100644 index 00000000000..16243fe9b74 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableSortedCellTableWithStylableHeaders.java @@ -0,0 +1,11 @@ +package com.sap.sailing.gwt.ui.client.shared.controls; + +import com.sap.sailing.gwt.ui.leaderboard.LeaderboardTableResources; +import com.sap.sailing.gwt.ui.leaderboard.SortedCellTableWithStylableHeaders; + +public class FlushableSortedCellTableWithStylableHeaders extends SortedCellTableWithStylableHeaders + implements Flushable { + public FlushableSortedCellTableWithStylableHeaders(int pageSize, LeaderboardTableResources resources) { + super(pageSize, resources); + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java index 3ed44bc3749..ccac732d016 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java @@ -48,6 +48,8 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW private final String checkboxColumnCellCSSClass; private final EventTranslator selectionEventTranslator; private final RefreshableMultiSelectionModel selectionModel; + private final ListDataProvider listDataProvider; + private final Flushable display; /** * @param selectedCheckboxCSSClass @@ -57,16 +59,25 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW * @param checkboxColumnCellCSSClass * CSS class for the <td> element rendering the cell * @param entityIdentityComparator - * {@link EntityIdentityComparator} to create a {@link RefreshableMultiSelectionModel}. + * {@link EntityIdentityComparator} to create a {@link RefreshableMultiSelectionModel} + * @param listDataProvider + * {@link ListDataProvider} to create a {@link RefreshableMultiSelectionModel} + * @param display + * {@link Flushable} to redraw the selected elements on the display */ protected SelectionCheckboxColumn(String selectedCheckboxCSSClass, String checkboxColumnCellCSSClass, - String deselectedCheckboxCSSClass, EntityIdentityComparator entityIdentityComparator) { + String deselectedCheckboxCSSClass, EntityIdentityComparator entityIdentityComparator, + ListDataProvider listDataProvider, Flushable display) { this(new BetterCheckboxCell(selectedCheckboxCSSClass, deselectedCheckboxCSSClass), checkboxColumnCellCSSClass, - entityIdentityComparator); + entityIdentityComparator, listDataProvider, display); } - private SelectionCheckboxColumn(BetterCheckboxCell checkboxCell, String checkboxColumnCellCSSClass, EntityIdentityComparator entityIdentityComparator) { + private SelectionCheckboxColumn(BetterCheckboxCell checkboxCell, String checkboxColumnCellCSSClass, + EntityIdentityComparator entityIdentityComparator, ListDataProvider listDataProvider, + Flushable display) { super(checkboxCell, SortingOrder.DESCENDING); + this.display = display; + this.listDataProvider = listDataProvider; this.cell = checkboxCell; this.checkboxColumnCellCSSClass = checkboxColumnCellCSSClass; this.selectionEventTranslator = createSelectionEventTranslator(); @@ -100,22 +111,19 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW * after selection changes. */ private RefreshableMultiSelectionModel createSelectionModel(final EntityIdentityComparator entityIdentityComparator) { - return new RefreshableMultiSelectionModel(entityIdentityComparator) { + return new RefreshableMultiSelectionModel(entityIdentityComparator, listDataProvider) { @Override public void clear() { super.clear(); - final List list = getListDataProvider().getList(); - for (T item : list) { - redrawRow(item, list); - } + display.flush(); } @Override public void setSelected(T item, boolean selected) { - final boolean redrawRequired = isSelected(item) != selected; + boolean wasSelected = isSelected(item); super.setSelected(item, selected); - if (redrawRequired) { - redrawRow(item, getListDataProvider().getList()); + if(wasSelected != selected) { + display.flush(); } } }; @@ -209,22 +217,4 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW } }; } - - /** - * Subclasses shall use this after an update to the selection state of a row to ensure the checkboxes are - * displayed correctly and consistently again. - * - * @param row - * the row object to be re-drawn - * @param dataList - * the result of calling {@link ListDataProvider#getList()} on the table's data provider which is - * actually a list wrapper where updates to which are reflected in the table display - */ - protected void redrawRow(final T row, final List dataList) { - int rowIndex = dataList.indexOf(row); - if (rowIndex >= 0) { - dataList.set(rowIndex, row); // trigger row redraw to have check box shown in correct state - } - getListDataProvider().flush(); - } } \ No newline at end of file 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 49d5dcf0ab9..1bb62efd0de 100755 --- 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 @@ -86,6 +86,7 @@ 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; @@ -101,6 +102,7 @@ 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.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.SimpleBusyIndicator; import com.sap.sse.gwt.client.player.PlayStateListener; @@ -161,7 +163,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS private final StringMessages stringMessages; - private final SortedCellTable leaderboardTable; + private final FlushableSortedCellTableWithStylableHeaders leaderboardTable; private final MultiSelectionModel leaderboardSelectionModel; @@ -1593,7 +1595,13 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS private class LeaderboardSelectionCheckboxColumn extends com.sap.sailing.gwt.ui.client.shared.controls.SelectionCheckboxColumn implements CompetitorSelectionChangeListener { protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) { - super(tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), null /*entityIdentityComparator to create a RefreshableSelectionModel*/); + super(tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), + tableResources.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { + @Override + public boolean representSameEntity(LeaderboardRowDTO dto1, LeaderboardRowDTO dto2) { + return dto1.competitor.getIdAsString().equals(dto2.competitor.getIdAsString()); + } + }, getData(), leaderboardTable); competitorSelectionProvider.addCompetitorSelectionChangeListener(this); } @@ -1611,28 +1619,30 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS */ @Override public void addedToSelection(CompetitorDTO competitor) { - final LeaderboardRowDTO row = getRow(competitor); + getSelectionModel().setSelected(getRow(competitor), true); +/* final LeaderboardRowDTO row = getRow(competitor); if (row != null) { redrawRow(row); - } + }*/ } - +/* private void redrawRow(final LeaderboardRowDTO row) { final List leaderboardDataList = getData().getList(); synchronized (leaderboardDataList) { redrawRow(row, leaderboardDataList); } } - +*/ /** * Ensure that the checkbox is redrawn when the competitor selection changes */ @Override public void removedFromSelection(CompetitorDTO competitor) { - final LeaderboardRowDTO row = getRow(competitor); + getSelectionModel().setSelected(getRow(competitor), false); +/* final LeaderboardRowDTO row = getRow(competitor); if (row != null) { redrawRow(row); - } + }*/ } @Override @@ -1770,7 +1780,6 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS break; } totalRankColumn = new TotalRankColumn(); - selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider); RACE_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableRaceColumnHeader(); LEG_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegColumnHeader(); LEG_DETAIL_COLUMN_HEADER_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumnHeader(); @@ -1778,7 +1787,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS LEG_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegColumn(); LEG_DETAIL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableLegDetailColumn(); TOTAL_COLUMN_STYLE = tableResources.cellTableStyle().cellTableTotalColumn(); - leaderboardTable = new SortedCellTableWithStylableHeaders( + leaderboardTable = new FlushableSortedCellTableWithStylableHeaders( /* pageSize */10000, tableResources); leaderboardTable.addCellPreviewHandler(new CellPreviewEvent.Handler() { @Override @@ -1792,6 +1801,7 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS } }); leaderboardTable.ensureDebugId("LeaderboardCellTable"); + selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider); getLeaderboardTable().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 From 1fe88aad63a14800012e731675b5e3c8a218e10a Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Tue, 8 Dec 2015 10:53:53 +0100 Subject: [PATCH 124/306] added PollsLiveDataEvery5Seconds interface for all ui components and dataretrievers --- .../client/PollsLiveDataEvery5Seconds.java | 12 +++ .../gwt/client/RibDashboardEntryPoint.java | 8 -- .../gwt/client/RibDashboardPanel.java | 73 +++++++++---------- .../dataretriever/WindBotDataRetriever.java | 12 ++- .../StartlineAnalysisComponent.java | 46 +++++------- ...StartLineAdvantageByGeometryComponent.java | 22 +++--- .../StartlineAdvantagesByWindComponent.java | 33 ++++----- 7 files changed, 100 insertions(+), 106 deletions(-) create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/PollsLiveDataEvery5Seconds.java diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/PollsLiveDataEvery5Seconds.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/PollsLiveDataEvery5Seconds.java new file mode 100644 index 00000000000..b64cb8f26bb --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/PollsLiveDataEvery5Seconds.java @@ -0,0 +1,12 @@ +package com.sap.sailing.dashboards.gwt.client; +import com.sap.sse.gwt.client.player.TimeListener; + + +/** + * @author Alexander Ries (D062114) + * + */ +public interface PollsLiveDataEvery5Seconds extends TimeListener { + + public void registerForDashboardFiveSecondsTimer(DashboardClientFactory dashboardClientFactory); +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java index cb19f17e03c..47ef8ad1c30 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardEntryPoint.java @@ -9,8 +9,6 @@ import com.googlecode.mgwt.ui.client.MGWTStyle; import com.sap.sailing.dashboards.gwt.client.dataretriever.WindBotDataRetriever; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.gwt.client.AbstractEntryPoint; -import com.sap.sse.gwt.client.player.Timer; -import com.sap.sse.gwt.client.player.Timer.PlayModes; /** * @@ -19,8 +17,6 @@ import com.sap.sse.gwt.client.player.Timer.PlayModes; */ public class RibDashboardEntryPoint extends AbstractEntryPoint { - private static final int DASHBOARD_RERFRESH_INTERVAL = 5000; - @Override public void doOnModuleLoad() { applyMGWTSettings(); @@ -29,10 +25,6 @@ public class RibDashboardEntryPoint extends AbstractEntryPoint { RootLayoutPanel.get().add(ribDashboardPanel); WindBotDataRetriever windBotDataRetriever = new WindBotDataRetriever(dashboardClientFactory); windBotDataRetriever.addNumberOfWindBotsChangeListeners(ribDashboardPanel); - Timer dashboardTimer = new Timer(PlayModes.Live); - dashboardTimer.setRefreshInterval(DASHBOARD_RERFRESH_INTERVAL); - dashboardTimer.addTimeListener(windBotDataRetriever); -// dashboardTimer.addTimeListener(dataRetriever); } private void applyMGWTSettings() { diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java index 7bf66d809f8..33b10654ba5 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/RibDashboardPanel.java @@ -31,9 +31,6 @@ import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; import com.sap.sailing.dashboards.gwt.shared.dto.RaceIdDTO; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; -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; /** * This class contains all components of the Dashboard and the basic UI Elements of the Dashboard, like top and bottom @@ -42,7 +39,7 @@ import com.sap.sse.gwt.client.player.Timer.PlayModes; * @author Alexander Ries * */ -public class RibDashboardPanel extends Composite implements NumberOfWindBotsChangeListener { +public class RibDashboardPanel extends Composite implements NumberOfWindBotsChangeListener, PollsLiveDataEvery5Seconds { interface RootUiBinder extends UiBinder { } @@ -105,9 +102,9 @@ public class RibDashboardPanel extends Composite implements NumberOfWindBotsChan windcharthint.getElement().setInnerText(stringConstants.dashboardWindChartHint()); windloadinghintleft.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
    "+stringConstants.dashboardWindBotLoadingMessage()); windloadinghintright.setInnerHTML(stringConstants.dashboardWindBotLoadingText()+"
    "+stringConstants.dashboardWindBotLoadingMessage()); - startUpdatingRaceLabelIn20SecondInterval(); loadAndAddEventLogo(dashboardClientFactory.getSailingService()); initAndAddWrongOrientationNotification(); + registerForDashboardFiveSecondsTimer(dashboardClientFactory); } private void initLogos() { @@ -174,44 +171,42 @@ public class RibDashboardPanel extends Composite implements NumberOfWindBotsChan } } - private void startUpdatingRaceLabelIn20SecondInterval() { - Timer timer = new Timer(PlayModes.Live); - timer.setRefreshInterval(5000); - timer.addTimeListener(new TimeListener() { - - @Override - public void timeChanged(Date newTime, Date oldTime) { - String leaderboardNameParameterValue = DashboardURLParameters.LEADERBOARD_NAME.getValue(); - if (leaderboardNameParameterValue != null) { - dashboardClientFactory.getDispatch().execute( - new GetIDFromRaceThatIsLiveAction(leaderboardNameParameterValue), - new AsyncCallback() { - - @Override - public void onSuccess(RaceIdDTO result) { - if (result != null && result.getRaceId() != null) { - logger.log(Level.INFO, "Updating UI with live race id"); - setHeaderText(result.getRaceId().getRaceName()); - } else { - logger.log(Level.INFO, "Received null for live race id"); - } - } - - @Override - public void onFailure(Throwable caught) { - logger.log(Level.INFO, "Failed to received live race id"+caught.getMessage()); - } - }); - } - } - }); - timer.play(); - } - private void setHeaderText(String raceName) { this.header.getElement().setInnerText(raceName); } + @Override + public void timeChanged(Date newTime, Date oldTime) { + String leaderboardNameParameterValue = DashboardURLParameters.LEADERBOARD_NAME.getValue(); + if (leaderboardNameParameterValue != null) { + dashboardClientFactory.getDispatch().execute( + new GetIDFromRaceThatIsLiveAction(leaderboardNameParameterValue), new AsyncCallback() { + + @Override + public void onSuccess(RaceIdDTO result) { + if (result != null && result.getRaceId() != null) { + logger.log(Level.INFO, "Updating UI with live race id"); + setHeaderText(result.getRaceId().getRaceName()); + } else { + logger.log(Level.INFO, "Received null for live race id"); + } + } + + @Override + public void onFailure(Throwable caught) { + logger.log(Level.INFO, "Failed to received live race id" + caught.getMessage()); + } + }); + } + } + + @Override + public void registerForDashboardFiveSecondsTimer(DashboardClientFactory dashboardClientFactory) { + if (dashboardClientFactory != null) { + dashboardClientFactory.getDashboardFiveSecondsTimer().addTimeListener(this); + } + } + @Override public void numberOfWindBotsReceivedChanged(List windBotIDs, WindBotDataRetrieverProvider windBotDataRetrieverProvider) { diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java index 6c97ddda791..be386047a20 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dataretriever/WindBotDataRetriever.java @@ -10,6 +10,7 @@ import java.util.logging.Logger; import com.google.gwt.user.client.rpc.AsyncCallback; import com.sap.sailing.dashboards.gwt.client.DashboardClientFactory; +import com.sap.sailing.dashboards.gwt.client.PollsLiveDataEvery5Seconds; import com.sap.sailing.dashboards.gwt.client.actions.GetIDFromRaceThatTakesWindFixesNowAction; import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; import com.sap.sailing.dashboards.gwt.shared.dto.RaceIdDTO; @@ -19,9 +20,8 @@ import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.gwt.ui.actions.GetWindInfoAction; import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; -import com.sap.sse.gwt.client.player.TimeListener; -public class WindBotDataRetriever implements TimeListener, WindBotDataRetrieverProvider { +public class WindBotDataRetriever implements PollsLiveDataEvery5Seconds, WindBotDataRetrieverProvider { private AsyncActionsExecutor asyncActionsExecutor; private List numberOfWindBotsChangeListeners; @@ -47,6 +47,7 @@ public class WindBotDataRetriever implements TimeListener, WindBotDataRetrieverP windBotIDsInLiveRace = new ArrayList(); windSourceTypeNames = new HashSet<>(); windSourceTypeNames.add(WindSourceType.EXPEDITION.name()); + registerForDashboardFiveSecondsTimer(dashboardClientFactory); } private void loadWindBotData(Date from, Date to, RegattaAndRaceIdentifier selectedRaceIdentifier) { @@ -169,4 +170,11 @@ public class WindBotDataRetriever implements TimeListener, WindBotDataRetrieverP } }); } + + @Override + public void registerForDashboardFiveSecondsTimer(DashboardClientFactory dashboardClientFactory) { + if (dashboardClientFactory != null) { + dashboardClientFactory.getDashboardFiveSecondsTimer().addTimeListener(this); + } + } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java index 4ec30c16e46..4730f54a5be 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startanalysis/StartlineAnalysisComponent.java @@ -24,6 +24,7 @@ import com.google.gwt.user.client.ui.Image; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.dashboards.gwt.client.DashboardClientFactory; +import com.sap.sailing.dashboards.gwt.client.PollsLiveDataEvery5Seconds; import com.sap.sailing.dashboards.gwt.client.RibDashboardImageResources; import com.sap.sailing.dashboards.gwt.client.actions.GetCompetitorInLeaderboardAction; import com.sap.sailing.dashboards.gwt.client.actions.GetStartAnalysesAction; @@ -38,9 +39,6 @@ import com.sap.sailing.dashboards.gwt.shared.dto.LeaderboardCompetitorsDTO; import com.sap.sailing.dashboards.gwt.shared.dto.StartAnalysesDTO; import com.sap.sailing.dashboards.gwt.shared.dto.startanalysis.StartAnalysisDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; -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; /** * The class contains an collection of {@link StartlineAnalysisCard}s that are displayed in horizontal aligned pages. It @@ -52,7 +50,7 @@ import com.sap.sse.gwt.client.player.Timer.PlayModes; * @author Alexander Ries (D062114) * */ -public class StartlineAnalysisComponent extends Composite implements HasWidgets { +public class StartlineAnalysisComponent extends Composite implements HasWidgets, PollsLiveDataEvery5Seconds { private static StartlineAnalysisComponentUiBinder uiBinder = GWT.create(StartlineAnalysisComponentUiBinder.class); @@ -106,9 +104,6 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets private List starts; private List pageChangeListener; private BottomNotification bottomNotification; - private Timer timer; - - private static final int RERFRESH_INTERVAL = 10000; private CompetitorSelectionPopup competitorSelectionPopup; private SettingsButtonWithSelectionIndicationLabel settingsButtonWithSelectionIndicationLabel; @@ -127,12 +122,9 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets this.dashboardClientFactory = dashboardClientFactory; pageChangeListener = new ArrayList(); starts = new ArrayList(); - initWidget(uiBinder.createAndBindUi(this)); - initCompetitorSelectionPopupAndAddCompetitorSelectionListener(); initLeftRightButtons(); - initTimer(); initAndAddBottomNotification(); getCachedSelectedCompetitorOrAskForWithPopup(); initAndAddSettingsButtonWithSelectionIndicationLabel(); @@ -155,23 +147,6 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets }); } - private void initTimer() { - timer = new Timer(PlayModes.Live); - timer.setRefreshInterval(RERFRESH_INTERVAL); - timer.addTimeListener(new TimeListener() { - - @Override - public void timeChanged(Date newTime, Date oldTime) { - String selectedCompetitorId = Cookies.getCookie(SELECTED_COMPETITOR_ID_COOKIE_KEY); - if (selectedCompetitorId != null) { - loadStartAnalysisDTOsForCompetitorID(selectedCompetitorId); - } else { - loadCompetitorsAndShowCompetitorSelectionPopup(); - } - } - }); - } - private void loadStartAnalysisDTOsForCompetitorID(String competitorIdAsString){ logger.log(Level.INFO, "Loading startanalysis for competitor id " + competitorIdAsString); dashboardClientFactory.getDispatch().execute(new GetStartAnalysesAction(DashboardURLParameters.LEADERBOARD_NAME.getValue(), competitorIdAsString), new AsyncCallback() { @@ -408,6 +383,23 @@ public class StartlineAnalysisComponent extends Composite implements HasWidgets return false; } } + + @Override + public void timeChanged(Date newTime, Date oldTime) { + String selectedCompetitorId = Cookies.getCookie(SELECTED_COMPETITOR_ID_COOKIE_KEY); + if (selectedCompetitorId != null) { + loadStartAnalysisDTOsForCompetitorID(selectedCompetitorId); + } else { + loadCompetitorsAndShowCompetitorSelectionPopup(); + } + } + + @Override + public void registerForDashboardFiveSecondsTimer(DashboardClientFactory dashboardClientFactory) { + if (dashboardClientFactory != null) { + dashboardClientFactory.getDashboardFiveSecondsTimer().addTimeListener(this); + } + } @Override public void add(Widget w) { diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartLineAdvantageByGeometryComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartLineAdvantageByGeometryComponent.java index 9e6c8ee4441..436965328b6 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartLineAdvantageByGeometryComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartLineAdvantageByGeometryComponent.java @@ -7,13 +7,11 @@ import java.util.logging.Logger; import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.user.client.rpc.AsyncCallback; import com.sap.sailing.dashboards.gwt.client.DashboardClientFactory; +import com.sap.sailing.dashboards.gwt.client.PollsLiveDataEvery5Seconds; import com.sap.sailing.dashboards.gwt.client.actions.GetStartlineAdvantageByGeometryAction; import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; import com.sap.sailing.dashboards.gwt.shared.dto.StartLineAdvantageDTO; import com.sap.sailing.gwt.ui.client.StringMessages; -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; /** * The purpose of the class is to display the live and average start line advantage either by wind or by geometry. There @@ -25,7 +23,7 @@ import com.sap.sse.gwt.client.player.Timer.PlayModes; * @author Alexander Ries (D062114) * */ -public class StartLineAdvantageByGeometryComponent extends LiveAverageComponent implements TimeListener { +public class StartLineAdvantageByGeometryComponent extends LiveAverageComponent implements PollsLiveDataEvery5Seconds { private DashboardClientFactory dashboardClientFactory; @@ -44,16 +42,9 @@ public class StartLineAdvantageByGeometryComponent extends LiveAverageComponent averagePanel.getElement().addClassName(StartLineAdvantageComponentRessources.INSTANCE.css().startLineAdvantageComponent_averagePanel()); this.liveLabel.setInnerHTML(stringConstants.dashboardLiveWind()); this.averageLabel.setInnerHTML(stringConstants.dashboardAverageWind()+"
    "+stringConstants.dashboardAverageWindMinutes(15)); - initSampleTimer(); + registerForDashboardFiveSecondsTimer(dashboardClientFactory); } - private void initSampleTimer(){ - Timer timer = new Timer(PlayModes.Live); - timer.setRefreshInterval(5000); - timer.addTimeListener(this); - timer.play(); - } - @Override public void timeChanged(Date newTime, Date oldTime) { String leaderboardNameParameterValue = DashboardURLParameters.LEADERBOARD_NAME.getValue(); @@ -80,4 +71,11 @@ public class StartLineAdvantageByGeometryComponent extends LiveAverageComponent }); } } + + @Override + public void registerForDashboardFiveSecondsTimer(DashboardClientFactory dashboardClientFactory) { + if (dashboardClientFactory != null) { + dashboardClientFactory.getDashboardFiveSecondsTimer().addTimeListener(this); + } + } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java index dd211ec9127..9601d98b5fa 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java @@ -16,19 +16,17 @@ import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.dashboards.gwt.client.DashboardClientFactory; +import com.sap.sailing.dashboards.gwt.client.PollsLiveDataEvery5Seconds; import com.sap.sailing.dashboards.gwt.client.actions.GetStartlineAdvantagesByWindAction; import com.sap.sailing.dashboards.gwt.shared.DashboardURLParameters; import com.sap.sailing.dashboards.gwt.shared.dto.StartlineAdvantagesWithMaxAndAverageDTO; import com.sap.sailing.gwt.ui.client.StringMessages; -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; /** * @author Alexander Ries (D062114) * */ -public class StartlineAdvantagesByWindComponent extends Composite implements HasWidgets, TimeListener { +public class StartlineAdvantagesByWindComponent extends Composite implements HasWidgets, PollsLiveDataEvery5Seconds { private static StartlineAdvantagesByWindComponentUiBinder uiBinder = GWT .create(StartlineAdvantagesByWindComponentUiBinder.class); @@ -60,7 +58,7 @@ public class StartlineAdvantagesByWindComponent extends Composite implements Has advantageMaximumLiveAverage.liveLabel.setInnerHTML("advantage max."); advantageMaximumLiveAverage.averageLabel.setInnerHTML("advantage max. average "+StringMessages.INSTANCE.dashboardAverageWindMinutes(15)); initWidget(uiBinder.createAndBindUi(this)); - initSampleTimer(); + registerForDashboardFiveSecondsTimer(dashboardClientFactory); } private void loadData() { @@ -94,14 +92,19 @@ public class StartlineAdvantagesByWindComponent extends Composite implements Has }); } } - - private void initSampleTimer() { - Timer timer = new Timer(PlayModes.Live); - timer.setRefreshInterval(5000); - timer.addTimeListener(this); - timer.play(); + + @Override + public void timeChanged(Date newTime, Date oldTime) { + loadData(); } - + + @Override + public void registerForDashboardFiveSecondsTimer(DashboardClientFactory dashboardClientFactory) { + if (dashboardClientFactory != null) { + dashboardClientFactory.getDashboardFiveSecondsTimer().addTimeListener(this); + } + } + @Override public void add(Widget w) { throw new UnsupportedOperationException("The method add(Widget w) is not supported."); @@ -121,10 +124,4 @@ public class StartlineAdvantagesByWindComponent extends Composite implements Has public boolean remove(Widget w) { return false; } - - @Override - public void timeChanged(Date newTime, Date oldTime) { - loadData(); - } - } \ No newline at end of file From df7d6cef13bc7352f017411cf876944d8b580803 Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Tue, 8 Dec 2015 11:18:24 +0100 Subject: [PATCH 125/306] sovled bug that causes sometimes minus values in LocationPointerCompass --- .../gwt/client/windchart/compass/LocationPointerCompass.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java index f51ed0e3238..526673bedcc 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java @@ -78,7 +78,8 @@ public class LocationPointerCompass extends AbsolutePanel implements HasWidgets, compassNeedle.getElement().getStyle().setProperty("transform", "rotate(" + winddirectionformatted + "deg)"); compassNeedle.getElement().getStyle() .setProperty("webkitTransform", "rotate(" + winddirectionformatted + "deg)"); - angleToPointetLocationLabel.setText(winddirectionformatted + "°"); + String winddirectionFormattedForLabel = NumberFormat.getFormat("#0").format((newDirection-angleOffset < 0) ? 360-newDirection-angleOffset : newDirection-angleOffset); + angleToPointetLocationLabel.setText(winddirectionFormattedForLabel+"°"); } } From e280949bc920604228fece61a37a1ad7ee5c54ac Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Tue, 8 Dec 2015 11:31:20 +0100 Subject: [PATCH 126/306] prettyfied bug fix for minus values in LocationPointerCompass --- .../client/windchart/compass/LocationPointerCompass.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java index 526673bedcc..ee97f1343b8 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java @@ -1,6 +1,7 @@ package com.sap.sailing.dashboards.gwt.client.windchart.compass; import com.google.gwt.i18n.client.NumberFormat; +import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.AbsolutePanel; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Image; @@ -74,11 +75,12 @@ public class LocationPointerCompass extends AbsolutePanel implements HasWidgets, private void updateNeedleAngle(double newDirection) { // Adapt degrees to image angle synchronized (this) { - String winddirectionformatted = NumberFormat.getFormat("#0").format(newDirection-angleOffset); + double newAngle = newDirection-angleOffset; + String winddirectionformatted = NumberFormat.getFormat("#0").format(newAngle); compassNeedle.getElement().getStyle().setProperty("transform", "rotate(" + winddirectionformatted + "deg)"); compassNeedle.getElement().getStyle() .setProperty("webkitTransform", "rotate(" + winddirectionformatted + "deg)"); - String winddirectionFormattedForLabel = NumberFormat.getFormat("#0").format((newDirection-angleOffset < 0) ? 360-newDirection-angleOffset : newDirection-angleOffset); + String winddirectionFormattedForLabel = NumberFormat.getFormat("#0").format((newAngle < 0) ? 360+newAngle : newAngle); angleToPointetLocationLabel.setText(winddirectionFormattedForLabel+"°"); } } From c08eaf2ea3efaf3231da9204d7692344578e745d Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Tue, 8 Dec 2015 11:35:04 +0100 Subject: [PATCH 127/306] removed unused imports in LocationPointerCompass --- .../gwt/client/windchart/compass/LocationPointerCompass.java | 1 - 1 file changed, 1 deletion(-) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java index ee97f1343b8..04cc49f0fb9 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/compass/LocationPointerCompass.java @@ -1,7 +1,6 @@ package com.sap.sailing.dashboards.gwt.client.windchart.compass; import com.google.gwt.i18n.client.NumberFormat; -import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.AbsolutePanel; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Image; From 374bad01d8eb980d2ebc114ccf5c342d1f7fbcb0 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 8 Dec 2015 12:46:56 +0100 Subject: [PATCH 128/306] use SecureRandom for user e-mail validation token generation --- .../.settings/org.eclipse.core.resources.prefs | 1 + java/com.sap.sse.security/src/com/sap/sse/security/User.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs index d6863f1498a..67d1a8e645f 100755 --- a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs +++ b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs @@ -7,3 +7,4 @@ encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties= encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties=UTF-8 +encoding//src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/AbstractResultsPresenter.java=utf-8 diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/User.java b/java/com.sap.sse.security/src/com/sap/sse/security/User.java index 147b7b729a1..a0cdbabc1bd 100644 --- a/java/com.sap.sse.security/src/com/sap/sse/security/User.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/User.java @@ -1,6 +1,7 @@ package com.sap.sse.security; import java.io.Serializable; +import java.security.SecureRandom; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; @@ -171,7 +172,7 @@ public class User implements Named, WithID { private String createRandomSecret() { final byte[] bytes1 = new byte[64]; - new Random().nextBytes(bytes1); + new SecureRandom().nextBytes(bytes1); final byte[] bytes2 = new byte[64]; new Random().nextBytes(bytes2); return new Sha256Hash(bytes1, bytes2, 1024).toBase64(); From 6ebc2376a4e5c1fc51f4c7063224cdf922bede15 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 8 Dec 2015 12:48:29 +0100 Subject: [PATCH 129/306] addedHomeServiceUtil to utf-8 sources --- .../.settings/org.eclipse.core.resources.prefs | 1 + 1 file changed, 1 insertion(+) diff --git a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs index 67d1a8e645f..0f7d005f58f 100755 --- a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs +++ b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs @@ -3,6 +3,7 @@ encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages.prop encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_de.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ru.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_zh.properties=UTF-8 +encoding//src/main/java/com/sap/sailing/gwt/server/HomeServiceUtil.java=utf-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties=UTF-8 From b6a0a243d847aa50aa3866b2e6ff72a273a8e130 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Tue, 8 Dec 2015 16:50:48 +0100 Subject: [PATCH 130/306] newSelectionModel: resolved a possible NullPointerException --- .../gwt/client/celltable/RefreshableMultiSelectionModel.java | 2 +- .../gwt/client/celltable/RefreshableSingleSelectionModel.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index c002e33621f..2ad17615d57 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -68,7 +68,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel */ @Override public void setSelected(T item, boolean selected) { - if (comp == null || dontcheckSelectionState) { + if (comp == null || dontcheckSelectionState || item == null || getSelectedSet() == null) { super.setSelected(item, selected); } else { T wasSelectedBefore = null; diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index d0b5eb07f6b..34c610fd05d 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -54,7 +54,7 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel @Override public void setSelected(T item, boolean selected) { - if (comp == null || dontcheckSelectionState) { + if (comp == null || dontcheckSelectionState || item == null || getSelectedObject() == null) { super.setSelected(item, selected); } else { if(comp.representSameEntity(getSelectedObject(), item)) { @@ -64,7 +64,7 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel super.setSelected(item, selected); } } - } + } private void refreshSelectionModel(Iterable newObjects) { dontcheckSelectionState = true; From b6888d1d7cdf5f2d085e2ef80fea6f6704509703 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 8 Dec 2015 16:57:22 +0100 Subject: [PATCH 131/306] removed com.sap.sailing.server.gateway.ess40 after checking back with Simon; it's not used anymore anywhere Change-Id: I623c627f549c0bb7c84ffa85ed4d84c35a67eb82 --- .../raceanalysis.product | 2 - java/com.sap.sailing.feature/feature.xml | 7 - .../.classpath | 7 - .../.gitignore | 1 - .../.project | 28 - .../.settings/org.eclipse.jdt.core.prefs | 12 - .../.settings/org.eclipse.pde.core.prefs | 3 - .../META-INF/MANIFEST.MF | 24 - .../WEB-INF/web.xml | 27 - .../build.properties | 7 - .../css/asc.gif | Bin 1108 -> 0 bytes .../css/bg.gif | Bin 1118 -> 0 bytes .../css/desc.gif | Bin 1108 -> 0 bytes .../css/leaderboard.css | 42 - .../css/row-bg.png | Bin 974 -> 0 bytes .../css/tablesorter.css | 58 - .../js/ess_act_leaderboard.html | 102 -- .../js/ess_series_leaderboard.html | 109 -- .../js/jquery-latest.js | 154 --- .../js/jquery.tablesorter.js | 1031 ----------------- .../js/jquery.tablesorter.min.js | 4 - .../pom.xml | 12 - .../ess40/ESS40ResultsAsCSVServlet.java | 196 ---- 23 files changed, 1826 deletions(-) delete mode 100644 java/com.sap.sailing.server.gateway.ess40/.classpath delete mode 100644 java/com.sap.sailing.server.gateway.ess40/.gitignore delete mode 100644 java/com.sap.sailing.server.gateway.ess40/.project delete mode 100644 java/com.sap.sailing.server.gateway.ess40/.settings/org.eclipse.jdt.core.prefs delete mode 100644 java/com.sap.sailing.server.gateway.ess40/.settings/org.eclipse.pde.core.prefs delete mode 100644 java/com.sap.sailing.server.gateway.ess40/META-INF/MANIFEST.MF delete mode 100644 java/com.sap.sailing.server.gateway.ess40/WEB-INF/web.xml delete mode 100644 java/com.sap.sailing.server.gateway.ess40/build.properties delete mode 100644 java/com.sap.sailing.server.gateway.ess40/css/asc.gif delete mode 100644 java/com.sap.sailing.server.gateway.ess40/css/bg.gif delete mode 100644 java/com.sap.sailing.server.gateway.ess40/css/desc.gif delete mode 100644 java/com.sap.sailing.server.gateway.ess40/css/leaderboard.css delete mode 100644 java/com.sap.sailing.server.gateway.ess40/css/row-bg.png delete mode 100644 java/com.sap.sailing.server.gateway.ess40/css/tablesorter.css delete mode 100644 java/com.sap.sailing.server.gateway.ess40/js/ess_act_leaderboard.html delete mode 100644 java/com.sap.sailing.server.gateway.ess40/js/ess_series_leaderboard.html delete mode 100644 java/com.sap.sailing.server.gateway.ess40/js/jquery-latest.js delete mode 100644 java/com.sap.sailing.server.gateway.ess40/js/jquery.tablesorter.js delete mode 100644 java/com.sap.sailing.server.gateway.ess40/js/jquery.tablesorter.min.js delete mode 100644 java/com.sap.sailing.server.gateway.ess40/pom.xml delete mode 100644 java/com.sap.sailing.server.gateway.ess40/src/com/sap/sailing/server/gateway/ess40/ESS40ResultsAsCSVServlet.java diff --git a/java/com.sap.sailing.feature.p2build/raceanalysis.product b/java/com.sap.sailing.feature.p2build/raceanalysis.product index 1dead91f56d..df90b607443 100755 --- a/java/com.sap.sailing.feature.p2build/raceanalysis.product +++ b/java/com.sap.sailing.feature.p2build/raceanalysis.product @@ -55,8 +55,6 @@ - - diff --git a/java/com.sap.sailing.feature/feature.xml b/java/com.sap.sailing.feature/feature.xml index fc426ce383c..ec021a0fddb 100755 --- a/java/com.sap.sailing.feature/feature.xml +++ b/java/com.sap.sailing.feature/feature.xml @@ -87,13 +87,6 @@ Computes leaderboard information for sailing races and offers RESTful APIs to al version="0.0.0" unpack="false"/> - - - - - - - - diff --git a/java/com.sap.sailing.server.gateway.ess40/.gitignore b/java/com.sap.sailing.server.gateway.ess40/.gitignore deleted file mode 100644 index 44519b5db33..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/.gitignore +++ /dev/null @@ -1 +0,0 @@ -bin/ diff --git a/java/com.sap.sailing.server.gateway.ess40/.project b/java/com.sap.sailing.server.gateway.ess40/.project deleted file mode 100644 index 856b75c9da9..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/.project +++ /dev/null @@ -1,28 +0,0 @@ - - - com.sap.sailing.server.gateway.ess40 - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.pde.ManifestBuilder - - - - - org.eclipse.pde.SchemaBuilder - - - - - - org.eclipse.pde.PluginNature - org.eclipse.jdt.core.javanature - - diff --git a/java/com.sap.sailing.server.gateway.ess40/.settings/org.eclipse.jdt.core.prefs b/java/com.sap.sailing.server.gateway.ess40/.settings/org.eclipse.jdt.core.prefs deleted file mode 100644 index a698e59674f..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/.settings/org.eclipse.jdt.core.prefs +++ /dev/null @@ -1,12 +0,0 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.8 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.8 diff --git a/java/com.sap.sailing.server.gateway.ess40/.settings/org.eclipse.pde.core.prefs b/java/com.sap.sailing.server.gateway.ess40/.settings/org.eclipse.pde.core.prefs deleted file mode 100644 index b7e72d019c2..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/.settings/org.eclipse.pde.core.prefs +++ /dev/null @@ -1,3 +0,0 @@ -eclipse.preferences.version=1 -pluginProject.extensions=false -resolve.requirebundle=false diff --git a/java/com.sap.sailing.server.gateway.ess40/META-INF/MANIFEST.MF b/java/com.sap.sailing.server.gateway.ess40/META-INF/MANIFEST.MF deleted file mode 100644 index f91c2c01d96..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/META-INF/MANIFEST.MF +++ /dev/null @@ -1,24 +0,0 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Gateway for ESS40 -Bundle-SymbolicName: com.sap.sailing.server.gateway.ess40 -Bundle-Version: 1.0.0.qualifier -Bundle-Vendor: SAP -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Import-Package: org.osgi.framework;version="1.3.0", - org.osgi.util.tracker;version="1.3.1", - com.sap.sailing.server -Bundle-ActivationPolicy: lazy -Require-Bundle: com.sap.sailing.domain, - com.sap.sailing.domain.tractracadapter, - com.sap.sailing.domain.persistence, - com.sap.sse.mongodb, - org.mongodb.mongo-java-driver;bundle-version="2.13.0", - javax.servlet;bundle-version="2.6.0", - com.sap.sailing.domain.common, - com.sap.sailing.server.gateway, - com.sap.sse.common, - com.sap.sse.replication, - com.sap.sse -Bundle-ClassPath: . -Web-ContextPath: /gateway-ess40 diff --git a/java/com.sap.sailing.server.gateway.ess40/WEB-INF/web.xml b/java/com.sap.sailing.server.gateway.ess40/WEB-INF/web.xml deleted file mode 100644 index 12863624767..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/WEB-INF/web.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - - dirAllowed - false - - - - - ESS40ResultsAsCSVServlet - com.sap.sailing.server.gateway.ess40.ESS40ResultsAsCSVServlet - - - ESS40ResultsAsCSVServlet - /csv/* - - diff --git a/java/com.sap.sailing.server.gateway.ess40/build.properties b/java/com.sap.sailing.server.gateway.ess40/build.properties deleted file mode 100644 index c70d0a9f4f4..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/build.properties +++ /dev/null @@ -1,7 +0,0 @@ -source.. = src/ -output.. = bin/ -bin.includes = META-INF/,\ - .,\ - WEB-INF/,\ - css/,\ - js/ diff --git a/java/com.sap.sailing.server.gateway.ess40/css/asc.gif b/java/com.sap.sailing.server.gateway.ess40/css/asc.gif deleted file mode 100644 index e4a4c5b3209c5ef9514baab1835aecb3d962f121..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1108 zcmZ?wbhEHb6lGvxXkcXc4+e_=xg&f76kHNZ5`naheMLcHa&~HoLQ-maW}dCm``!DM z6f#q6mBLMZ4SWlnQ!_F>s)|yBtNcQetFn_VQ=?Nk^)#sNw%$$BS=C4WT$g}QL2Kep0RGSfuW&-nVFuUiK&^Hp^k!) zfuWJU0T7w#8k$&{npqi{D?ot~(6*wKG^-#NH>h1eo~=?wNlAf~zJ7Umxn8-kUVc%! zzM-Y1CCCgTBVC{h-Qvo;lEez#ykcdT2`;I{$wiq3C7Jno3Lp~`lk!VTY?Xj6g?J&i z0B&qvu^!kvddc~@`W1-<`i6Q2ATPlb`ugHmnwtw(U0f2Bif}JhgNs8d3sUuiQj7CT zi;`1;%9TM@6<9eJr6!i-7lq{K=fFZSAS1sdzc?emK*2fKOhLmpF*!32B%le_;p=PV znO9trn3tUD>0+w{G(#^lGsViy$l1iw+|t#|+|k0&(ACJ%&BV>o&D`AC(!#{p)yxH^ z*Cju>G&eP`1g19yq1O$kUQlAlEdbi=l3J8mmYU*Ll%J~r_Ow+d7PpugS(us{xdF|y zbjI!$h~5+|ZUO5xHZZ`cS0CsYeNfaQMKw$an0`P^c)|s8;7LC<518JIfC-!N|G&R~ ze*gOUo>*T>t-)5G1()y3J#(ZSx%*2db( z(!$)#)Wq1x&_G{LS4Ue*Q$t-%RYh4zQ9)i#Rz_M%QbJrzR76-vP=KG0mxr5+lY^a& rm4%s!k%2++Ckv2dWYA#%0#LcZz{J}lzkvJ2`CGHoFJ`OgGFSru#S@4p diff --git a/java/com.sap.sailing.server.gateway.ess40/css/bg.gif b/java/com.sap.sailing.server.gateway.ess40/css/bg.gif deleted file mode 100644 index b0f66561832c9e4cd2170860445c20fc48e71f96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1118 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 BH|PKW diff --git a/java/com.sap.sailing.server.gateway.ess40/css/tablesorter.css b/java/com.sap.sailing.server.gateway.ess40/css/tablesorter.css deleted file mode 100644 index 0ebf819c0a6..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/css/tablesorter.css +++ /dev/null @@ -1,58 +0,0 @@ -/* table sorter table */ -table.tablesorter { - font-family:arial; - background-color: #CDCDCD; - margin:10px 0pt 15px; - font-size: 8pt; - text-align: left; -} - -table.tablesorter thead tr th, table.tablesorter tfoot tr th { - background-color: #4d4d4d; - border-bottom: 1px solid #4d4d4d; - font-size: 12px; - padding: 4px; - color: #fff; - border-right: 1px solid #9c9c9c; -} - -table.tablesorter thead tr .header { - background-image: url(bg.gif); - background-repeat: no-repeat; - background-position: center right; - cursor: pointer; - padding-left: 10px; - padding-right: 20px; -} - -table.tablesorter tbody td { - color: #fff; - padding: 0 4px 4px 15px; - background: url(row-bg.png); - vertical-align: middle; - border-right: 1px solid #9c9c9c; - font-size: 12px; - font-variant: small-caps; - height: 36px; - font-weight: bold; -} - -table.tablesorter tbody tr:last-child td { - height: 29px; -} - -table.tablesorter tbody tr.odd td { - background-color:#F0F0F6; -} - -table.tablesorter thead tr .headerSortUp { - background-image: url(asc.gif); -} - -table.tablesorter thead tr .headerSortDown { - background-image: url(desc.gif); -} - -table.tablesorter thead tr .headerSortDown, table.tablesorter thead tr .headerSortUp { - background-color: #949494; -} diff --git a/java/com.sap.sailing.server.gateway.ess40/js/ess_act_leaderboard.html b/java/com.sap.sailing.server.gateway.ess40/js/ess_act_leaderboard.html deleted file mode 100644 index 12171eccbc7..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/js/ess_act_leaderboard.html +++ /dev/null @@ -1,102 +0,0 @@ - - - - Extreme Sailing Series leaderboard - - - - - - - - - -

    -

    -
    - - - \ No newline at end of file diff --git a/java/com.sap.sailing.server.gateway.ess40/js/ess_series_leaderboard.html b/java/com.sap.sailing.server.gateway.ess40/js/ess_series_leaderboard.html deleted file mode 100644 index c99c0d6fdad..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/js/ess_series_leaderboard.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - Extreme Sailing Series leaderboard - - - - - - - - - -

    -

    -
    - - - \ No newline at end of file diff --git a/java/com.sap.sailing.server.gateway.ess40/js/jquery-latest.js b/java/com.sap.sailing.server.gateway.ess40/js/jquery-latest.js deleted file mode 100644 index ac7e7009dc9..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/js/jquery-latest.js +++ /dev/null @@ -1,154 +0,0 @@ -/*! - * jQuery JavaScript Library v1.4.2 - * http://jquery.com/ - * - * Copyright 2010, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * Copyright 2010, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * - * Date: Sat Feb 13 22:33:48 2010 -0500 - */ -(function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/, -Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&& -(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this, -a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b=== -"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this, -function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b
    a"; -var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected, -parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent= -false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n= -s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true, -applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando]; -else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this, -a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b=== -w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i, -cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected= -c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); -a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g, -function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split("."); -k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a), -C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B=0){a.type= -e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&& -f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive; -if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data", -e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a, -"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a, -d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, -e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift(); -t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D|| -g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()}, -CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m, -g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)}, -text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}}, -setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return hl[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h= -h[3];l=0;for(m=h.length;l=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m=== -"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g, -h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&& -q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML=""; -if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="

    ";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}(); -(function(){var g=s.createElement("div");g.innerHTML="
    ";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}: -function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f0)for(var j=d;j0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j= -{},i;if(f&&a.length){e=0;for(var o=a.length;e-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a=== -"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode", -d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")? -a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType=== -1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/"},F={option:[1,""],legend:[1,"
    ","
    "],thead:[1,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],col:[2,"","
    "],area:[1,"",""],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div
    ","
    "];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= -c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, -wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, -prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, -this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); -return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja, -""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]); -return this}else{e=0;for(var j=d.length;e0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["", -""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]===""&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e= -c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]? -c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja= -function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter= -Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a, -"border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f= -a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b= -a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=//gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!== -"string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("
    ").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this}, -serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), -function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href, -global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&& -e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)? -"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache=== -false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B= -false;C.onload=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){B=true;b();d();C.onload=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since", -c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E|| -d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call(x); -g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status=== -1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b=== -"json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional; -if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration=== -"number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]|| -c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start; -this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now= -this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem, -e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b
    "; -a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b); -c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a, -d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top- -f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset": -"pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in -e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window); diff --git a/java/com.sap.sailing.server.gateway.ess40/js/jquery.tablesorter.js b/java/com.sap.sailing.server.gateway.ess40/js/jquery.tablesorter.js deleted file mode 100644 index 9b587312261..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/js/jquery.tablesorter.js +++ /dev/null @@ -1,1031 +0,0 @@ -/* - * - * TableSorter 2.0 - Client-side table sorting with ease! - * Version 2.0.5b - * @requires jQuery v1.2.3 - * - * Copyright (c) 2007 Christian Bach - * Examples and docs at: http://tablesorter.com - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - */ -/** - * - * @description Create a sortable table with multi-column sorting capabilitys - * - * @example $('table').tablesorter(); - * @desc Create a simple tablesorter interface. - * - * @example $('table').tablesorter({ sortList:[[0,0],[1,0]] }); - * @desc Create a tablesorter interface and sort on the first and secound column column headers. - * - * @example $('table').tablesorter({ headers: { 0: { sorter: false}, 1: {sorter: false} } }); - * - * @desc Create a tablesorter interface and disableing the first and second column headers. - * - * - * @example $('table').tablesorter({ headers: { 0: {sorter:"integer"}, 1: {sorter:"currency"} } }); - * - * @desc Create a tablesorter interface and set a column parser for the first - * and second column. - * - * - * @param Object - * settings An object literal containing key/value pairs to provide - * optional settings. - * - * - * @option String cssHeader (optional) A string of the class name to be appended - * to sortable tr elements in the thead of the table. Default value: - * "header" - * - * @option String cssAsc (optional) A string of the class name to be appended to - * sortable tr elements in the thead on a ascending sort. Default value: - * "headerSortUp" - * - * @option String cssDesc (optional) A string of the class name to be appended - * to sortable tr elements in the thead on a descending sort. Default - * value: "headerSortDown" - * - * @option String sortInitialOrder (optional) A string of the inital sorting - * order can be asc or desc. Default value: "asc" - * - * @option String sortMultisortKey (optional) A string of the multi-column sort - * key. Default value: "shiftKey" - * - * @option String textExtraction (optional) A string of the text-extraction - * method to use. For complex html structures inside td cell set this - * option to "complex", on large tables the complex option can be slow. - * Default value: "simple" - * - * @option Object headers (optional) An array containing the forces sorting - * rules. This option let's you specify a default sorting rule. Default - * value: null - * - * @option Array sortList (optional) An array containing the forces sorting - * rules. This option let's you specify a default sorting rule. Default - * value: null - * - * @option Array sortForce (optional) An array containing forced sorting rules. - * This option let's you specify a default sorting rule, which is - * prepended to user-selected rules. Default value: null - * - * @option Boolean sortLocaleCompare (optional) Boolean flag indicating whatever - * to use String.localeCampare method or not. Default set to true. - * - * - * @option Array sortAppend (optional) An array containing forced sorting rules. - * This option let's you specify a default sorting rule, which is - * appended to user-selected rules. Default value: null - * - * @option Boolean widthFixed (optional) Boolean flag indicating if tablesorter - * should apply fixed widths to the table columns. This is usefull when - * using the pager companion plugin. This options requires the dimension - * jquery plugin. Default value: false - * - * @option Boolean cancelSelection (optional) Boolean flag indicating if - * tablesorter should cancel selection of the table headers text. - * Default value: true - * - * @option Boolean debug (optional) Boolean flag indicating if tablesorter - * should display debuging information usefull for development. - * - * @type jQuery - * - * @name tablesorter - * - * @cat Plugins/Tablesorter - * - * @author Christian Bach/christian.bach@polyester.se - */ - -(function ($) { - $.extend({ - tablesorter: new - function () { - - var parsers = [], - widgets = []; - - this.defaults = { - cssHeader: "header", - cssAsc: "headerSortUp", - cssDesc: "headerSortDown", - cssChildRow: "expand-child", - sortInitialOrder: "asc", - sortMultiSortKey: "shiftKey", - sortForce: null, - sortAppend: null, - sortLocaleCompare: true, - textExtraction: "simple", - parsers: {}, widgets: [], - widgetZebra: { - css: ["even", "odd"] - }, headers: {}, widthFixed: false, - cancelSelection: true, - sortList: [], - headerList: [], - dateFormat: "us", - decimal: '/\.|\,/g', - onRenderHeader: null, - selectorHeaders: 'thead th', - debug: false - }; - - /* debuging utils */ - - function benchmark(s, d) { - log(s + "," + (new Date().getTime() - d.getTime()) + "ms"); - } - - this.benchmark = benchmark; - - function log(s) { - if (typeof console != "undefined" && typeof console.debug != "undefined") { - console.log(s); - } else { - alert(s); - } - } - - /* parsers utils */ - - function buildParserCache(table, $headers) { - - if (table.config.debug) { - var parsersDebug = ""; - } - - if (table.tBodies.length == 0) return; // In the case of empty tables - var rows = table.tBodies[0].rows; - - if (rows[0]) { - - var list = [], - cells = rows[0].cells, - l = cells.length; - - for (var i = 0; i < l; i++) { - - var p = false; - - if ($.metadata && ($($headers[i]).metadata() && $($headers[i]).metadata().sorter)) { - - p = getParserById($($headers[i]).metadata().sorter); - - } else if ((table.config.headers[i] && table.config.headers[i].sorter)) { - - p = getParserById(table.config.headers[i].sorter); - } - if (!p) { - - p = detectParserForColumn(table, rows, -1, i); - } - - if (table.config.debug) { - parsersDebug += "column:" + i + " parser:" + p.id + "\n"; - } - - list.push(p); - } - } - - if (table.config.debug) { - log(parsersDebug); - } - - return list; - }; - - function detectParserForColumn(table, rows, rowIndex, cellIndex) { - var l = parsers.length, - node = false, - nodeValue = false, - keepLooking = true; - while (nodeValue == '' && keepLooking) { - rowIndex++; - if (rows[rowIndex]) { - node = getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex); - nodeValue = trimAndGetNodeText(table.config, node); - if (table.config.debug) { - log('Checking if value was empty on row:' + rowIndex); - } - } else { - keepLooking = false; - } - } - for (var i = 1; i < l; i++) { - if (parsers[i].is(nodeValue, table, node)) { - return parsers[i]; - } - } - // 0 is always the generic parser (text) - return parsers[0]; - } - - function getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex) { - return rows[rowIndex].cells[cellIndex]; - } - - function trimAndGetNodeText(config, node) { - return $.trim(getElementText(config, node)); - } - - function getParserById(name) { - var l = parsers.length; - for (var i = 0; i < l; i++) { - if (parsers[i].id.toLowerCase() == name.toLowerCase()) { - return parsers[i]; - } - } - return false; - } - - /* utils */ - - function buildCache(table) { - - if (table.config.debug) { - var cacheTime = new Date(); - } - - var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0, - totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0, - parsers = table.config.parsers, - cache = { - row: [], - normalized: [] - }; - - for (var i = 0; i < totalRows; ++i) { - - /** Add the table data to main data array */ - var c = $(table.tBodies[0].rows[i]), - cols = []; - - // if this is a child row, add it to the last row's children and - // continue to the next row - if (c.hasClass(table.config.cssChildRow)) { - cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add(c); - // go to the next for loop - continue; - } - - cache.row.push(c); - - for (var j = 0; j < totalCells; ++j) { - cols.push(parsers[j].format(getElementText(table.config, c[0].cells[j]), table, c[0].cells[j])); - } - - cols.push(cache.normalized.length); // add position for rowCache - cache.normalized.push(cols); - cols = null; - }; - - if (table.config.debug) { - benchmark("Building cache for " + totalRows + " rows:", cacheTime); - } - - return cache; - }; - - function getElementText(config, node) { - - var text = ""; - - if (!node) return ""; - - if (!config.supportsTextContent) config.supportsTextContent = node.textContent || false; - - if (config.textExtraction == "simple") { - if (config.supportsTextContent) { - text = node.textContent; - } else { - if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) { - text = node.childNodes[0].innerHTML; - } else { - text = node.innerHTML; - } - } - } else { - if (typeof(config.textExtraction) == "function") { - text = config.textExtraction(node); - } else { - text = $(node).text(); - } - } - return text; - } - - function appendToTable(table, cache) { - - if (table.config.debug) { - var appendTime = new Date() - } - - var c = cache, - r = c.row, - n = c.normalized, - totalRows = n.length, - checkCell = (n[0].length - 1), - tableBody = $(table.tBodies[0]), - rows = []; - - - for (var i = 0; i < totalRows; i++) { - var pos = n[i][checkCell]; - - rows.push(r[pos]); - - if (!table.config.appender) { - - //var o = ; - var l = r[pos].length; - for (var j = 0; j < l; j++) { - tableBody[0].appendChild(r[pos][j]); - } - - // - } - } - - - - if (table.config.appender) { - - table.config.appender(table, rows); - } - - rows = null; - - if (table.config.debug) { - benchmark("Rebuilt table:", appendTime); - } - - // apply table widgets - applyWidget(table); - - // trigger sortend - setTimeout(function () { - $(table).trigger("sortEnd"); - }, 0); - - }; - - function buildHeaders(table) { - - if (table.config.debug) { - var time = new Date(); - } - - var meta = ($.metadata) ? true : false; - - var header_index = computeTableHeaderCellIndexes(table); - - $tableHeaders = $(table.config.selectorHeaders, table).each(function (index) { - - this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; - // this.column = index; - this.order = formatSortingOrder(table.config.sortInitialOrder); - - - this.count = this.order; - - if (checkHeaderMetadata(this) || checkHeaderOptions(table, index)) this.sortDisabled = true; - if (checkHeaderOptionsSortingLocked(table, index)) this.order = this.lockedOrder = checkHeaderOptionsSortingLocked(table, index); - - if (!this.sortDisabled) { - var $th = $(this).addClass(table.config.cssHeader); - if (table.config.onRenderHeader) table.config.onRenderHeader.apply($th); - } - - // add cell to headerList - table.config.headerList[index] = this; - }); - - if (table.config.debug) { - benchmark("Built headers:", time); - log($tableHeaders); - } - - return $tableHeaders; - - }; - - // from: - // http://www.javascripttoolbox.com/lib/table/examples.php - // http://www.javascripttoolbox.com/temp/table_cellindex.html - - - function computeTableHeaderCellIndexes(t) { - var matrix = []; - var lookup = {}; - var thead = t.getElementsByTagName('THEAD')[0]; - var trs = thead.getElementsByTagName('TR'); - - for (var i = 0; i < trs.length; i++) { - var cells = trs[i].cells; - for (var j = 0; j < cells.length; j++) { - var c = cells[j]; - - var rowIndex = c.parentNode.rowIndex; - var cellId = rowIndex + "-" + c.cellIndex; - var rowSpan = c.rowSpan || 1; - var colSpan = c.colSpan || 1 - var firstAvailCol; - if (typeof(matrix[rowIndex]) == "undefined") { - matrix[rowIndex] = []; - } - // Find first available column in the first row - for (var k = 0; k < matrix[rowIndex].length + 1; k++) { - if (typeof(matrix[rowIndex][k]) == "undefined") { - firstAvailCol = k; - break; - } - } - lookup[cellId] = firstAvailCol; - for (var k = rowIndex; k < rowIndex + rowSpan; k++) { - if (typeof(matrix[k]) == "undefined") { - matrix[k] = []; - } - var matrixrow = matrix[k]; - for (var l = firstAvailCol; l < firstAvailCol + colSpan; l++) { - matrixrow[l] = "x"; - } - } - } - } - return lookup; - } - - function checkCellColSpan(table, rows, row) { - var arr = [], - r = table.tHead.rows, - c = r[row].cells; - - for (var i = 0; i < c.length; i++) { - var cell = c[i]; - - if (cell.colSpan > 1) { - arr = arr.concat(checkCellColSpan(table, headerArr, row++)); - } else { - if (table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row + 1])) { - arr.push(cell); - } - // headerArr[row] = (i+row); - } - } - return arr; - }; - - function checkHeaderMetadata(cell) { - if (($.metadata) && ($(cell).metadata().sorter === false)) { - return true; - }; - return false; - } - - function checkHeaderOptions(table, i) { - if ((table.config.headers[i]) && (table.config.headers[i].sorter === false)) { - return true; - }; - return false; - } - - function checkHeaderOptionsSortingLocked(table, i) { - if ((table.config.headers[i]) && (table.config.headers[i].lockedOrder)) return table.config.headers[i].lockedOrder; - return false; - } - - function applyWidget(table) { - var c = table.config.widgets; - var l = c.length; - for (var i = 0; i < l; i++) { - - getWidgetById(c[i]).format(table); - } - - } - - function getWidgetById(name) { - var l = widgets.length; - for (var i = 0; i < l; i++) { - if (widgets[i].id.toLowerCase() == name.toLowerCase()) { - return widgets[i]; - } - } - }; - - function formatSortingOrder(v) { - if (typeof(v) != "Number") { - return (v.toLowerCase() == "desc") ? 1 : 0; - } else { - return (v == 1) ? 1 : 0; - } - } - - function isValueInArray(v, a) { - var l = a.length; - for (var i = 0; i < l; i++) { - if (a[i][0] == v) { - return true; - } - } - return false; - } - - function setHeadersCss(table, $headers, list, css) { - // remove all header information - $headers.removeClass(css[0]).removeClass(css[1]); - - var h = []; - $headers.each(function (offset) { - if (!this.sortDisabled) { - h[this.column] = $(this); - } - }); - - var l = list.length; - for (var i = 0; i < l; i++) { - h[list[i][0]].addClass(css[list[i][1]]); - } - } - - function fixColumnWidth(table, $headers) { - var c = table.config; - if (c.widthFixed) { - var colgroup = $(''); - $("tr:first td", table.tBodies[0]).each(function () { - colgroup.append($('').css('width', $(this).width())); - }); - $(table).prepend(colgroup); - }; - } - - function updateHeaderSortCount(table, sortList) { - var c = table.config, - l = sortList.length; - for (var i = 0; i < l; i++) { - var s = sortList[i], - o = c.headerList[s[0]]; - o.count = s[1]; - o.count++; - } - } - - /* sorting methods */ - - function multisort(table, sortList, cache) { - - if (table.config.debug) { - var sortTime = new Date(); - } - - var dynamicExp = "var sortWrapper = function(a,b) {", - l = sortList.length; - - // TODO: inline functions. - for (var i = 0; i < l; i++) { - - var c = sortList[i][0]; - var order = sortList[i][1]; - // var s = (getCachedSortType(table.config.parsers,c) == "text") ? - // ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? - // "sortNumeric" : "sortNumericDesc"); - // var s = (table.config.parsers[c].type == "text") ? ((order == 0) - // ? makeSortText(c) : makeSortTextDesc(c)) : ((order == 0) ? - // makeSortNumeric(c) : makeSortNumericDesc(c)); - var s = (table.config.parsers[c].type == "text") ? ((order == 0) ? makeSortFunction("text", "asc", c) : makeSortFunction("text", "desc", c)) : ((order == 0) ? makeSortFunction("numeric", "asc", c) : makeSortFunction("numeric", "desc", c)); - var e = "e" + i; - - dynamicExp += "var " + e + " = " + s; // + "(a[" + c + "],b[" + c - // + "]); "; - dynamicExp += "if(" + e + ") { return " + e + "; } "; - dynamicExp += "else { "; - - } - - // if value is the same keep orignal order - var orgOrderCol = cache.normalized[0].length - 1; - dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; - - for (var i = 0; i < l; i++) { - dynamicExp += "}; "; - } - - dynamicExp += "return 0; "; - dynamicExp += "}; "; - - if (table.config.debug) { - benchmark("Evaling expression:" + dynamicExp, new Date()); - } - - eval(dynamicExp); - - cache.normalized.sort(sortWrapper); - - if (table.config.debug) { - benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time:", sortTime); - } - - return cache; - }; - - function makeSortFunction(type, direction, index) { - var a = "a[" + index + "]", - b = "b[" + index + "]"; - if (type == 'text' && direction == 'asc') { - return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + a + " < " + b + ") ? -1 : 1 )));"; - } else if (type == 'text' && direction == 'desc') { - return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + b + " < " + a + ") ? -1 : 1 )));"; - } else if (type == 'numeric' && direction == 'asc') { - return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + a + " - " + b + "));"; - } else if (type == 'numeric' && direction == 'desc') { - return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + b + " - " + a + "));"; - } - }; - - function makeSortText(i) { - return "((a[" + i + "] < b[" + i + "]) ? -1 : ((a[" + i + "] > b[" + i + "]) ? 1 : 0));"; - }; - - function makeSortTextDesc(i) { - return "((b[" + i + "] < a[" + i + "]) ? -1 : ((b[" + i + "] > a[" + i + "]) ? 1 : 0));"; - }; - - function makeSortNumeric(i) { - return "a[" + i + "]-b[" + i + "];"; - }; - - function makeSortNumericDesc(i) { - return "b[" + i + "]-a[" + i + "];"; - }; - - function sortText(a, b) { - if (table.config.sortLocaleCompare) return a.localeCompare(b); - return ((a < b) ? -1 : ((a > b) ? 1 : 0)); - }; - - function sortTextDesc(a, b) { - if (table.config.sortLocaleCompare) return b.localeCompare(a); - return ((b < a) ? -1 : ((b > a) ? 1 : 0)); - }; - - function sortNumeric(a, b) { - return a - b; - }; - - function sortNumericDesc(a, b) { - return b - a; - }; - - function getCachedSortType(parsers, i) { - return parsers[i].type; - }; /* public methods */ - this.construct = function (settings) { - return this.each(function () { - // if no thead or tbody quit. - if (!this.tHead || !this.tBodies) return; - // declare - var $this, $document, $headers, cache, config, shiftDown = 0, - sortOrder; - // new blank config object - this.config = {}; - // merge and extend. - config = $.extend(this.config, $.tablesorter.defaults, settings); - // store common expression for speed - $this = $(this); - // save the settings where they read - $.data(this, "tablesorter", config); - // build headers - $headers = buildHeaders(this); - // try to auto detect column type, and store in tables config - this.config.parsers = buildParserCache(this, $headers); - // build the cache for the tbody cells - cache = buildCache(this); - // get the css class names, could be done else where. - var sortCSS = [config.cssDesc, config.cssAsc]; - // fixate columns if the users supplies the fixedWidth option - fixColumnWidth(this); - // apply event handling to headers - // this is to big, perhaps break it out? - $headers.click( - - function (e) { - var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0; - if (!this.sortDisabled && totalRows > 0) { - // Only call sortStart if sorting is - // enabled. - $this.trigger("sortStart"); - // store exp, for speed - var $cell = $(this); - // get current column index - var i = this.column; - // get current column sort order - this.order = this.count++ % 2; - // always sort on the locked order. - if(this.lockedOrder) this.order = this.lockedOrder; - - // user only whants to sort on one - // column - if (!e[config.sortMultiSortKey]) { - // flush the sort list - config.sortList = []; - if (config.sortForce != null) { - var a = config.sortForce; - for (var j = 0; j < a.length; j++) { - if (a[j][0] != i) { - config.sortList.push(a[j]); - } - } - } - // add column to sort list - config.sortList.push([i, this.order]); - // multi column sorting - } else { - // the user has clicked on an all - // ready sortet column. - if (isValueInArray(i, config.sortList)) { - // revers the sorting direction - // for all tables. - for (var j = 0; j < config.sortList.length; j++) { - var s = config.sortList[j], - o = config.headerList[s[0]]; - if (s[0] == i) { - o.count = s[1]; - o.count++; - s[1] = o.count % 2; - } - } - } else { - // add column to sort list array - config.sortList.push([i, this.order]); - } - }; - setTimeout(function () { - // set css for headers - setHeadersCss($this[0], $headers, config.sortList, sortCSS); - appendToTable( - $this[0], multisort( - $this[0], config.sortList, cache) - ); - }, 1); - // stop normal event by returning false - return false; - } - // cancel selection - }).mousedown(function () { - if (config.cancelSelection) { - this.onselectstart = function () { - return false - }; - return false; - } - }); - // apply easy methods that trigger binded events - $this.bind("update", function () { - var me = this; - setTimeout(function () { - // rebuild parsers. - me.config.parsers = buildParserCache( - me, $headers); - // rebuild the cache map - cache = buildCache(me); - }, 1); - }).bind("updateCell", function (e, cell) { - var config = this.config; - // get position from the dom. - var pos = [(cell.parentNode.rowIndex - 1), cell.cellIndex]; - // update cache - cache.normalized[pos[0]][pos[1]] = config.parsers[pos[1]].format( - getElementText(config, cell), cell); - }).bind("sorton", function (e, list) { - $(this).trigger("sortStart"); - config.sortList = list; - // update and store the sortlist - var sortList = config.sortList; - // update header count index - updateHeaderSortCount(this, sortList); - // set css for headers - setHeadersCss(this, $headers, sortList, sortCSS); - // sort the table and append it to the dom - appendToTable(this, multisort(this, sortList, cache)); - }).bind("appendCache", function () { - appendToTable(this, cache); - }).bind("applyWidgetId", function (e, id) { - getWidgetById(id).format(this); - }).bind("applyWidgets", function () { - // apply widgets - applyWidget(this); - }); - if ($.metadata && ($(this).metadata() && $(this).metadata().sortlist)) { - config.sortList = $(this).metadata().sortlist; - } - // if user has supplied a sort list to constructor. - if (config.sortList.length > 0) { - $this.trigger("sorton", [config.sortList]); - } - // apply widgets - applyWidget(this); - }); - }; - this.addParser = function (parser) { - var l = parsers.length, - a = true; - for (var i = 0; i < l; i++) { - if (parsers[i].id.toLowerCase() == parser.id.toLowerCase()) { - a = false; - } - } - if (a) { - parsers.push(parser); - }; - }; - this.addWidget = function (widget) { - widgets.push(widget); - }; - this.formatFloat = function (s) { - var i = parseFloat(s); - return (isNaN(i)) ? 0 : i; - }; - this.formatInt = function (s) { - var i = parseInt(s); - return (isNaN(i)) ? 0 : i; - }; - this.isDigit = function (s, config) { - // replace all an wanted chars and match. - return /^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g, ''))); - }; - this.clearTableBody = function (table) { - if ($.browser.msie) { - function empty() { - while (this.firstChild) - this.removeChild(this.firstChild); - } - empty.apply(table.tBodies[0]); - } else { - table.tBodies[0].innerHTML = ""; - } - }; - } - }); - - // extend plugin scope - $.fn.extend({ - tablesorter: $.tablesorter.construct - }); - - // make shortcut - var ts = $.tablesorter; - - // add default parsers - ts.addParser({ - id: "text", - is: function (s) { - return true; - }, format: function (s) { - return $.trim(s.toLocaleLowerCase()); - }, type: "text" - }); - - ts.addParser({ - id: "digit", - is: function (s, table) { - var c = table.config; - return $.tablesorter.isDigit(s, c); - }, format: function (s) { - return $.tablesorter.formatFloat(s); - }, type: "numeric" - }); - - ts.addParser({ - id: "currency", - is: function (s) { - return /^[£$€?.]/.test(s); - }, format: function (s) { - return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g), "")); - }, type: "numeric" - }); - - ts.addParser({ - id: "ipAddress", - is: function (s) { - return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s); - }, format: function (s) { - var a = s.split("."), - r = "", - l = a.length; - for (var i = 0; i < l; i++) { - var item = a[i]; - if (item.length == 2) { - r += "0" + item; - } else { - r += item; - } - } - return $.tablesorter.formatFloat(r); - }, type: "numeric" - }); - - ts.addParser({ - id: "url", - is: function (s) { - return /^(https?|ftp|file):\/\/$/.test(s); - }, format: function (s) { - return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//), '')); - }, type: "text" - }); - - ts.addParser({ - id: "isoDate", - is: function (s) { - return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s); - }, format: function (s) { - return $.tablesorter.formatFloat((s != "") ? new Date(s.replace( - new RegExp(/-/g), "/")).getTime() : "0"); - }, type: "numeric" - }); - - ts.addParser({ - id: "percent", - is: function (s) { - return /\%$/.test($.trim(s)); - }, format: function (s) { - return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g), "")); - }, type: "numeric" - }); - - ts.addParser({ - id: "usLongDate", - is: function (s) { - return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/)); - }, format: function (s) { - return $.tablesorter.formatFloat(new Date(s).getTime()); - }, type: "numeric" - }); - - ts.addParser({ - id: "shortDate", - is: function (s) { - return /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s); - }, format: function (s, table) { - var c = table.config; - s = s.replace(/\-/g, "/"); - if (c.dateFormat == "us") { - // reformat the string in ISO format - s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2"); - } else if (c.dateFormat == "uk") { - // reformat the string in ISO format - s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1"); - } else if (c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") { - s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3"); - } - return $.tablesorter.formatFloat(new Date(s).getTime()); - }, type: "numeric" - }); - ts.addParser({ - id: "time", - is: function (s) { - return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s); - }, format: function (s) { - return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime()); - }, type: "numeric" - }); - ts.addParser({ - id: "metadata", - is: function (s) { - return false; - }, format: function (s, table, cell) { - var c = table.config, - p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; - return $(cell).metadata()[p]; - }, type: "numeric" - }); - // add default widgets - ts.addWidget({ - id: "zebra", - format: function (table) { - if (table.config.debug) { - var time = new Date(); - } - var $tr, row = -1, - odd; - // loop through the visible rows - $("tr:visible", table.tBodies[0]).each(function (i) { - $tr = $(this); - // style children rows the same way the parent - // row was styled - if (!$tr.hasClass(table.config.cssChildRow)) row++; - odd = (row % 2 == 0); - $tr.removeClass( - table.config.widgetZebra.css[odd ? 0 : 1]).addClass( - table.config.widgetZebra.css[odd ? 1 : 0]) - }); - if (table.config.debug) { - $.tablesorter.benchmark("Applying Zebra widget", time); - } - } - }); -})(jQuery); \ No newline at end of file diff --git a/java/com.sap.sailing.server.gateway.ess40/js/jquery.tablesorter.min.js b/java/com.sap.sailing.server.gateway.ess40/js/jquery.tablesorter.min.js deleted file mode 100644 index f81aafb6e4d..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/js/jquery.tablesorter.min.js +++ /dev/null @@ -1,4 +0,0 @@ - -(function($){$.extend({tablesorter:new -function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",cssChildRow:"expand-child",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,sortLocaleCompare:true,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'/\.|\,/g',onRenderHeader:null,selectorHeaders:'thead th',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}if(table.tBodies.length==0)return;var rows=table.tBodies[0].rows;if(rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function checkHeaderOptionsSortingLocked(table,i){if((table.config.headers[i])&&(table.config.headers[i].lockedOrder))return table.config.headers[i].lockedOrder;return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;i b["+i+"]) ? 1 : 0));";};function makeSortTextDesc(i){return"((b["+i+"] < a["+i+"]) ? -1 : ((b["+i+"] > a["+i+"]) ? 1 : 0));";};function makeSortNumeric(i){return"a["+i+"]-b["+i+"];";};function makeSortNumericDesc(i){return"b["+i+"]-a["+i+"];";};function sortText(a,b){if(table.config.sortLocaleCompare)return a.localeCompare(b);return((ab)?1:0));};function sortTextDesc(a,b){if(table.config.sortLocaleCompare)return b.localeCompare(a);return((ba)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$.data(this,"tablesorter",config);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){$this.trigger("sortStart");var $cell=$(this);var i=this.column;this.order=this.count++%2;if(this.lockedOrder)this.order=this.lockedOrder;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;i - - 4.0.0 - - root - com.sap.sailing - 1.0.0-SNAPSHOT - - com.sap.sailing.server.gateway.ess40 - eclipse-plugin - diff --git a/java/com.sap.sailing.server.gateway.ess40/src/com/sap/sailing/server/gateway/ess40/ESS40ResultsAsCSVServlet.java b/java/com.sap.sailing.server.gateway.ess40/src/com/sap/sailing/server/gateway/ess40/ESS40ResultsAsCSVServlet.java deleted file mode 100644 index 64cab6d02c9..00000000000 --- a/java/com.sap.sailing.server.gateway.ess40/src/com/sap/sailing/server/gateway/ess40/ESS40ResultsAsCSVServlet.java +++ /dev/null @@ -1,196 +0,0 @@ -package com.sap.sailing.server.gateway.ess40; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.text.DecimalFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.RaceColumn; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.domain.leaderboard.Leaderboard; -import com.sap.sailing.server.RacingEventService; -import com.sap.sailing.server.gateway.AbstractCSVHttpServlet; -import com.sap.sse.common.TimePoint; -import com.sap.sse.common.impl.MillisecondsTimePoint; - -/** - * Exports leaderboards of the ESS40 Series to a specific .csv format which is used by a native iOS app to show them. - * Sample csv files can be found at http://www.extremesailingseries.com/app/results/csv_uploads/ - * - * @author Frank - * - */ -public class ESS40ResultsAsCSVServlet extends AbstractCSVHttpServlet { - private static final long serialVersionUID = -3975664653148197608L; - private static final String PARAM_NAME_LEADERBOARDNAME = "leaderboardName"; - private static final String PARAM_NAME_RESULTSTATE = "resultState"; - private enum ResultStates { Live, Preliminary, Final }; - private final ResultStates DEFAULT_RESULT_STATE = ResultStates.Final; - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - RacingEventService racingEventService = getService(); - String pathInfo = request.getPathInfo(); - String leaderboardNameParam = request.getParameter(PARAM_NAME_LEADERBOARDNAME); - - if (leaderboardNameParam == null) { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, - "You need to specify a leaderboard name using the " + PARAM_NAME_LEADERBOARDNAME + " parameter"); - return; - } - - Leaderboard leaderboard = racingEventService.getLeaderboardByName(leaderboardNameParam); - - if (pathInfo.equals("/leaderboard") && leaderboard != null) { - try { - String fileName = leaderboardNameParam + ".csv"; - List> csv = new ArrayList>(); - - setCSVResponseHeader(response, fileName); - - ResultStates resultState = resolveRequestedResultState(request.getParameter(PARAM_NAME_RESULTSTATE)); - TimePoint requestTimePoint = MillisecondsTimePoint.now(); - TimePoint resultTimePoint = calculateTimePointForResultState(leaderboard, resultState); - - // competitors are ordered according to total rank - List suppressedCompetitors = new ArrayList(); - for (Competitor c : leaderboard.getSuppressedCompetitors()) { - suppressedCompetitors.add(c); - } - List competitorsFromBestToWorst = leaderboard.getCompetitorsFromBestToWorst(resultTimePoint != null ? resultTimePoint : requestTimePoint); - Map> rankedCompetitorsPerColumn = new HashMap>(); - Iterable raceColumns = leaderboard.getRaceColumns(); - - if(resultTimePoint != null) { - for (Competitor competitor : competitorsFromBestToWorst) { - if (!suppressedCompetitors.contains(competitor)) { - // int totalRank = competitorsFromBestToWorst.indexOf(competitor) + 1; - Double totalPoints = leaderboard.getTotalPoints(competitor, resultTimePoint); - if (leaderboard.hasCarriedPoints(competitor)) { - Double carriedPoints = leaderboard.getCarriedPoints(competitor); - if (carriedPoints != null) { - totalPoints = totalPoints != null ? totalPoints += carriedPoints : carriedPoints; - } - } - - List csvLine = new ArrayList(); - String competitorDisplayName = leaderboard.getDisplayName(competitor); - csvLine.add(competitorDisplayName != null ? competitorDisplayName : competitor.getName()); - csv.add(csvLine); - - // TODO: we should only export complete races where all competitors have valid totalPoints for a race - for (RaceColumn raceColumn : raceColumns) { - List rankedCompetitorsForColumn = rankedCompetitorsPerColumn.get(raceColumn); - if (rankedCompetitorsForColumn == null) { - rankedCompetitorsForColumn = leaderboard.getCompetitorsFromBestToWorst(raceColumn, - resultTimePoint); - rankedCompetitorsPerColumn.put(raceColumn, rankedCompetitorsForColumn); - } - // Double netRacePoints = leaderboard.getNetPoints(competitor, raceColumn, timePoint); - Double totalRacePoints = leaderboard.getTotalPoints(competitor, raceColumn, resultTimePoint); - MaxPointsReason maxPointsReason = leaderboard.getMaxPointsReason(competitor, raceColumn, resultTimePoint); - int rank = rankedCompetitorsForColumn.indexOf(competitor)+1; - if(totalRacePoints != null) { - if (maxPointsReason == null || maxPointsReason == MaxPointsReason.NONE) { - csvLine.add(rank); - csvLine.add(totalRacePoints); - - } else { - csvLine.add(maxPointsReason.name()); - csvLine.add(totalRacePoints); - } - } else { - /* Make sure to also include null columns */ - csvLine.add(0); - csvLine.add(0); - } - } - } - } - } else { - for (Competitor competitor : competitorsFromBestToWorst) { - if (!suppressedCompetitors.contains(competitor)) { - List csvLine = new ArrayList(); - String competitorDisplayName = leaderboard.getDisplayName(competitor); - csvLine.add(competitorDisplayName != null ? competitorDisplayName : competitor.getName()); - csv.add(csvLine); - } - } - } - writeCsv(csv, ',', true, response.getOutputStream()); - } catch (Exception e) { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error during leaderboard export"); - } - } else { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Error during leaderboard export"); - } - } - - @Override - protected void writeCsv (List> csv, char separator, boolean quoteStrings, OutputStream output) throws IOException { - DecimalFormat df = new DecimalFormat("0.##"); - BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, "UTF-8")); - for (List row : csv) { - StringBuilder line = new StringBuilder(); - int column = 1; - for (Iterator iter = row.iterator(); iter.hasNext();) { - T fieldObject = iter.next(); - String field = String.valueOf(fieldObject).replace("\"", "\"\""); - if(fieldObject instanceof String && column == 1) { - field = '"' + field + '"'; - } else if(fieldObject instanceof Double) { - field = df.format(fieldObject); - } - line.append(field); - if (iter.hasNext()) { - line.append(separator); - } - column++; - } - writer.write(line.toString()); - writer.newLine(); - } - writer.flush(); - } - - private ResultStates resolveRequestedResultState(String resultStateParam) { - ResultStates result = DEFAULT_RESULT_STATE; - if(resultStateParam != null) { - for(ResultStates state: ResultStates.values()) { - if(state.name().equalsIgnoreCase(resultStateParam)) { - result = state; - break; - } - } - } - return result; - } - - private TimePoint calculateTimePointForResultState(Leaderboard leaderboard, ResultStates resultState) { - TimePoint result = null; - switch (resultState) { - case Live: - result = MillisecondsTimePoint.now(); - break; - case Preliminary: - case Final: - if(leaderboard.getScoreCorrection() != null) { - result = leaderboard.getScoreCorrection().getTimePointOfLastCorrectionsValidity(); - } - break; - } - - return result; - } -} From 4ce384abe06c1ac4a07f1e18cf9c6de29039f5b2 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Tue, 8 Dec 2015 17:01:45 +0100 Subject: [PATCH 132/306] newSelectionModel: changes on LeaderboardConfigPanels --- .../adminconsole/AbstractLeaderboardConfigPanel.java | 11 ++--------- .../gwt/ui/adminconsole/LeaderboardConfigPanel.java | 1 - 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index 19ca712de3c..fe7093034f8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -326,7 +326,6 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement availableLeaderboardList.clear(); Util.addAll(leaderboards, availableLeaderboardList); filterLeaderboardPanel.updateAll(availableLeaderboardList); // also maintains the filtered leaderboardList - leaderboardRaceColumnSelectionChanged(); } /** @@ -339,15 +338,9 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement new AsyncCallback() { @Override public void onSuccess(StrippedLeaderboardDTO leaderboard) { - for (StrippedLeaderboardDTO leaderboardDTO : refreshableLeaderboardSelectionModel.getSelectedSet()) { - if (leaderboardDTO.name.equals(leaderboardName)) { - refreshableLeaderboardSelectionModel.setSelected(leaderboardDTO, false); - break; - } - } replaceLeaderboardInList(availableLeaderboardList, leaderboardName, leaderboard); filterLeaderboardPanel.updateAll(availableLeaderboardList); // also updates leaderboardList provider - refreshableLeaderboardSelectionModel.setSelected(leaderboard, true); // TODO Lukas: should/will this replace a previously selected element that is compared equal by the EntityIdentityComparator? + refreshableLeaderboardSelectionModel.setSelected(leaderboard, true); if (nameOfRaceColumnToSelect != null) { selectRaceColumn(nameOfRaceColumnToSelect); } @@ -362,7 +355,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement } })); } - +//TODO If the list does not contains the element the last element will be replaced? private void replaceLeaderboardInList(List leaderboardList, String leaderboardToReplace, StrippedLeaderboardDTO newLeaderboard) { int index = -1; for (StrippedLeaderboardDTO existingLeaderboard : leaderboardList) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java index 898eef6a3a4..939d847dfec 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java @@ -854,6 +854,5 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { private void removeLeaderboardFromTable(final StrippedLeaderboardDTO leaderBoard) { leaderboardList.getList().remove(leaderBoard); availableLeaderboardList.remove(leaderBoard); - refreshableLeaderboardSelectionModel.setSelected(leaderBoard, false); } } \ No newline at end of file From dc8bc246eda511b728496576a2f5e994f44e2303 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 8 Dec 2015 18:08:44 +0100 Subject: [PATCH 133/306] set hoverlines to non-transparent by default; increased weight from 10 to 15 Change-Id: I2e5c153d51a826473126b330f64ad2001788c36f --- .../sailing/gwt/ui/client/shared/racemap/RaceMapSettings.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettings.java index ef42bc32951..e47cdb01de3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapSettings.java @@ -20,9 +20,9 @@ public class RaceMapSettings extends AbstractSettings { private RaceMapHelpLinesSettings helpLinesSettings; - private boolean transparentHoverlines = true; + private boolean transparentHoverlines = false; // as discussed with Stefan on 2015-12-08 - private int hoverlineStrokeWeight = 10; + private int hoverlineStrokeWeight = 15; // as discussed with Stefan on 2015-12-08 private long tailLengthInMilliseconds = 100000l; From 3f419fbd3dbc300f784554b79579cce9cee8dfb1 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 9 Dec 2015 08:45:08 +0100 Subject: [PATCH 134/306] newSelectionModel: changed SelectionCheckboxColumn#getListDataProvider to non abstract method and modified Javadoc --- .../AbstractLeaderboardConfigPanel.java | 4 --- .../AbstractTrackedRacesListComposite.java | 6 +--- .../ui/adminconsole/EventListComposite.java | 4 --- .../LeaderboardGroupConfigPanel.java | 12 ++------ .../ui/adminconsole/RegattaListComposite.java | 5 ---- .../StructureImportListComposite.java | 6 ---- .../gwt/ui/adminconsole/TableWrapper.java | 4 --- .../TracTracEventManagementPanel.java | 5 ---- .../controls/SelectionCheckboxColumn.java | 29 +++++++++---------- .../gwt/ui/leaderboard/LeaderboardPanel.java | 5 ---- 10 files changed, 16 insertions(+), 64 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index fe7093034f8..77e713a90a9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -306,10 +306,6 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement return dto1.name.equals(dto2.name); } }, listDataProvider, leaderboardTable) { - @Override - protected ListDataProvider getListDataProvider() { - return leaderboardList; - } }; selectionCheckboxColumn.setSortable(true); leaderboardColumnListHandler.setComparator(selectionCheckboxColumn, new Comparator() { 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 413ca5ead14..f63d37a782a 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 @@ -128,11 +128,7 @@ public abstract class AbstractTrackedRacesListComposite extends SimplePanel impl tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), entityIdentityComparator, raceList, raceTable) { - @Override - protected ListDataProvider getListDataProvider() { - return raceList; - } - + @Override public Boolean getValue(RaceDTO row) { return raceTable.getSelectionModel().isSelected(row); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 414037d64cf..d8177850a07 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -195,10 +195,6 @@ public class EventListComposite extends Composite implements EventsRefresher, Le return dto1.id.equals(dto2.id); } },eventListDataProvider,table) { - @Override - protected ListDataProvider getListDataProvider() { - return eventListDataProvider; - } @Override public Boolean getValue(EventDTO row) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java index a8029ec374f..131b53e9464 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java @@ -203,10 +203,6 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements return dto1.name.equals(dto2.name); } }, leaderboardsProvider, leaderboardsTable) { - @Override - protected ListDataProvider getListDataProvider() { - return leaderboardsProvider; - } }; refreshableLeaderboardsSelectionModel = leaderboardTableSelectionColumn.getSelectionModel(); leaderboardsTable.setSelectionModel(refreshableLeaderboardsSelectionModel, leaderboardTableSelectionColumn.getSelectionManager()); @@ -375,18 +371,14 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel implements groupDetailsProvider.addDataDisplay(groupDetailsTable); SelectionCheckboxColumn groupDetailsTableSelectionColumn = new SelectionCheckboxColumn( - tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), tableResources.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { - + tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), + tableResources.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { @Override public boolean representSameEntity(StrippedLeaderboardDTO dto1, StrippedLeaderboardDTO dto2) { return dto1.name.equals(dto2.name); } }, groupDetailsProvider, groupDetailsTable) { - @Override - protected ListDataProvider getListDataProvider() { - return groupDetailsProvider; - } }; groupDetailsTable.setWidth("100%"); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java index ecc94aaa8a8..707b55f5ce5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RegattaListComposite.java @@ -139,11 +139,6 @@ public class RegattaListComposite extends Composite implements RegattasDisplayer return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); } },regattaListDataProvider, table) { - @Override - protected ListDataProvider getListDataProvider() { - return regattaListDataProvider; - } - @Override public Boolean getValue(RegattaDTO row) { return regattaTable.getSelectionModel().isSelected(row); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java index e50fcc1cdae..3a8b8a8b7cb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportListComposite.java @@ -5,7 +5,6 @@ import java.util.Comparator; import com.google.gwt.user.cellview.client.CellTable; import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; import com.google.gwt.user.cellview.client.TextColumn; -import com.google.gwt.view.client.ListDataProvider; import com.sap.sailing.gwt.ui.client.RegattaRefresher; import com.sap.sailing.gwt.ui.client.RegattasDisplayer; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; @@ -49,11 +48,6 @@ public class StructureImportListComposite extends RegattaListComposite implement return dto1.getRegattaIdentifier().equals(dto2.getRegattaIdentifier()); } }, regattaListDataProvider, table) { - @Override - protected ListDataProvider getListDataProvider() { - return regattaListDataProvider; - } - @Override public Boolean getValue(RegattaDTO row) { return regattaTable.getSelectionModel().isSelected(row); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java index b0c2ac85ade..0c4d41fa54e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java @@ -54,10 +54,6 @@ public abstract class TableWrapper> im tableRes.cellTableStyle().cellTableCheckboxSelected(), tableRes.cellTableStyle().cellTableCheckboxDeselected(), tableRes.cellTableStyle().cellTableCheckboxColumnCell(), this.entityIdentityComparator, dataProvider, table) { - @Override - protected ListDataProvider getListDataProvider() { - return dataProvider; - } }; columnSortHandler.setComparator(selectionCheckboxColumn, selectionCheckboxColumn.getComparator()); @SuppressWarnings("unchecked") diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java index c498cb1640c..b102277711d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java @@ -393,11 +393,6 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { public Boolean getValue(TracTracRaceRecordDTO row) { return racesTable.getSelectionModel().isSelected(row); } - - @Override - protected ListDataProvider getListDataProvider() { - return raceList; - } }; racesTable.addColumn(selectionCheckboxColumn, selectionCheckboxColumn.getHeader()); racesTable.addColumn(regattaNameColumn, stringMessages.event()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java index ccac732d016..a0f1bcaf40b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java @@ -1,6 +1,5 @@ package com.sap.sailing.gwt.ui.client.shared.controls; -import java.util.List; import com.google.gwt.cell.client.Cell.Context; import com.google.gwt.dom.client.BrowserEvents; import com.google.gwt.dom.client.NativeEvent; @@ -16,7 +15,6 @@ import com.google.gwt.view.client.DefaultSelectionEventManager.SelectAction; import com.google.gwt.view.client.ListDataProvider; import com.sap.sailing.domain.common.InvertibleComparator; import com.sap.sailing.domain.common.SortingOrder; -import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; import com.sap.sailing.domain.common.impl.InvertibleComparatorAdapter; import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; @@ -30,9 +28,11 @@ import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; * updates work properly. *

    * - * If clients don't choose to use this columns own {@link #getSelectionModel() selection model}, they have to ensure - * that selection changes with subsequent {@link #redrawRow(Object, List) redraw requests} are triggered properly upon - * selection status changes. Otherwise, the checkboxes will run out of sync with the selection status. + * Clients should use the columns own {@link #getSelectionModel() RefreshableMultiSelectionModel}. This will ensure that + * the {@link SelectionCheckboxColumn} will be refreshed correctly, when the selection changes or the + * {@link ListDataProvider} have new elements. Clients should also ensure that the {@link Flushable display} and the + * {@link ListDataProvider} is not null otherwise the {@link RefreshableMultiSelectionModel selectionmodel} + * won´t work correct. *

    * * The column uses the {@link BetterCheckboxCell} cell to implement the display properties. Three CSS styles can be used @@ -84,13 +84,6 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW this.selectionModel = createSelectionModel(entityIdentityComparator); } - /** - * Subclasses need to tell this column what the data list is. This is necessary in order to reach all rows after the - * selection is cleared, as well as to trigger the redraw of a row by setting it again in the list data provider which - * triggers the necessary redraw operation. - */ - protected abstract ListDataProvider getListDataProvider(); - /** * @return a selection manager that should be used for the table to which this column is added; use * {@link CellTable#setSelectionModel(com.google.gwt.view.client.SelectionModel, com.google.gwt.view.client.CellPreviewEvent.Handler)} @@ -99,16 +92,20 @@ public abstract class SelectionCheckboxColumn extends AbstractSortableColumnW public CellPreviewEvent.Handler getSelectionManager() { return DefaultSelectionEventManager.createCustomManager(getSelectionEventTranslator()); } + + protected ListDataProvider getListDataProvider() { + return listDataProvider; + } public RefreshableMultiSelectionModel getSelectionModel() { return selectionModel; } /** - * Clients should use the multi-selection model returned by this method for the {@link CellTable} to which they add this column. - * If they do so, the {@link #redrawRow(LeaderboardRowDTO, List)} method will be triggered correctly for all selection changes. - * Otherwise, clients or subclasses are responsible to issue the necessary calls to {@link #redrawRow(LeaderboardRowDTO, List)} - * after selection changes. + * Clients should use the {@link RefreshableMultiSelectionModel} returned by this method for the {@link CellTable} + * to which they add this column. If they do so, the {@link Flushable#flush()} method will be + * triggered correctly for all selection changes. Otherwise, clients or subclasses are responsible to issue the + * necessary calls to {@link Flushable#flush()} after selection changes. */ private RefreshableMultiSelectionModel createSelectionModel(final EntityIdentityComparator entityIdentityComparator) { return new RefreshableMultiSelectionModel(entityIdentityComparator, listDataProvider) { 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 1bb62efd0de..1a9f0e136f4 100755 --- 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 @@ -1647,11 +1647,6 @@ public class LeaderboardPanel extends SimplePanel implements TimeListener, PlayS @Override public void updateMinMax() {} - - @Override - protected ListDataProvider getListDataProvider() { - return getData(); - } } private class TotalRankColumn extends LeaderboardSortableColumnWithMinMax { From 562c6b33e30856b367abcf9ee3b2d4184a1cda14 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 9 Dec 2015 10:28:50 +0100 Subject: [PATCH 135/306] newSelectionModel: modified Javadoc in RefeshableSelectionModels --- .../RefreshableMultiSelectionModel.java | 62 ++++++++++++++----- .../celltable/RefreshableSelectionModel.java | 41 +++++------- .../RefreshableSingleSelectionModel.java | 57 +++++++++++++++-- 3 files changed, 114 insertions(+), 46 deletions(-) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java index 2ad17615d57..11055c02646 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableMultiSelectionModel.java @@ -5,27 +5,30 @@ import java.util.List; import java.util.Set; import com.google.gwt.event.shared.HandlerRegistration; -import com.google.gwt.user.cellview.client.CellTable; import com.google.gwt.view.client.HasData; import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.MultiSelectionModel; import com.google.gwt.view.client.ProvidesKey; import com.google.gwt.view.client.Range; import com.google.gwt.view.client.RangeChangeEvent.Handler; +import com.google.gwt.view.client.SelectionModel.AbstractSelectionModel; import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SelectionModel; /** - * This {@link RefreshableMultiSelectionModel} have the property that it is refreshable. That means if the - * {@link CellTable} have new Elements you can call - * {@link RefreshableMultiSelectionModel#refreshSelectionModel(Iterable) refreshSelectionModel(Iterable)} and the - * elements with {@link EntityIdentityComparator#representSameEntity(Object, Object) the same identity} will be - * reselected. + * This {@link RefreshableMultiSelectionModel} implements the {@link RefreshableSelectionModel} interface. So it + * register it self as a display on the {@link ListDataProvider} and reacts on the changes of {@link ListDataProvider}. + * When the {@link ListDataProvider} is changed this {@link RefreshableMultiSelectionModel selectionmodel} will refresh + * the selection according to the {@link ListDataProvider} changes. To make this class work correct it is very important + * to set the {@link ListDataProvider}, otherwise it won´t work. + *

    + * For more details on the update process read the {@link RefreshableSelectionModel} Javadoc and see the methods + * {@link RefreshableMultiSelectionModel#refreshSelectionModel(Iterable)} and + * {@link RefreshableMultiSelectionModel#setRowData(int, List)}. * + * @author D064976 * @param - * the type of {@link CellTable} entries - * - * TODO /FIXME Describe interaction with ListDataProvider + * the type of entries */ public class RefreshableMultiSelectionModel extends MultiSelectionModel implements RefreshableSelectionModel, HasData { @@ -36,6 +39,9 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel /** * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects + * @param listDataProvider + * {@link ListDataProvider} to add this {@link RefreshableSingleSelectionModel selectionmodel} as an + * display on {@link ListDataProvider} */ public RefreshableMultiSelectionModel(EntityIdentityComparator comp, ListDataProvider listDataProvider) { super(); @@ -49,6 +55,9 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel * {@link ProvidesKey} for the super class constructor * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects + * @param listDataProvider + * {@link ListDataProvider} to add this {@link RefreshableSingleSelectionModel selectionmodel} as an + * display on {@link ListDataProvider} */ public RefreshableMultiSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp, ListDataProvider listDataProvider) { super(keyProvider); @@ -57,14 +66,21 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel this.listDataProvider.addDataDisplay(this); } + /** + * @return the {@link EntityIdentityComparator} for the {@link RefreshableSingleSelectionModel}. If the + * {@link EntityIdentityComparator} is not set this method will return null. + */ @Override public EntityIdentityComparator getEntityIdentityComparator() { return comp; } - /* - * TODO / FIXME: Need to redefine setSelected here: if an element is set selected for which another element is in - * the current selection that is compared equal by the EntityIdentityComparator then it should be replaced + /** + * Checks the old selection state of the object. If it was selected before, the old version will be replaced with + * the new one. In all other cases this method behave same as + * super.setSelected(T item, boolean selected). + *

    + * When the {@link EntityIdentityComparator} is null this method also behaves like the super method */ @Override public void setSelected(T item, boolean selected) { @@ -90,10 +106,20 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel } /** - * refreshes the {@link RefreshableMultiSelectionModel SelectionModel} with all elements of the - * {@link ListDataProvider} + * Refreshes the {@link RefreshableMultiSelectionModel} with the newObjects.All objects from the + * current selection that {@link EntityIdentityComparator#representSameEntity(Object, Object) represent the same + * entity} as an object from newObjects will be reselected. All others are de-selected. That means a + * selected object is not contained in newObjects the object wouldn't be selected anymore. If this + * selection model has no {@link EntityIdentityComparator} set, this method will use the {@link #equals(Object)} + * method to compare. If an object is reselected it will be replaced with the new version of it. *

    - * FIXME change to private + * + * When the selection is refreshed this method triggers a + * {@link SelectionChangeEvent.Handler#onSelectionChange(SelectionChangeEvent) onSelectionChangedEvent} using + * {@link AbstractSelectionModel#fireEvent(com.google.gwt.event.shared.GwtEvent)}. + * + * @param newObjects + * the new objects to refresh the {@link RefreshableMultiSelectionModel selectionmodel} */ private void refreshSelectionModel(Iterable newObjects) { dontcheckSelectionState = true; @@ -119,7 +145,11 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel } dontcheckSelectionState = false; } - + + /** + * This method is called when the {@link ListDataProvider} has new elements. It takes the new elements and refreshes + * the {@link RefreshableMultiSelectionModel selectionmodel} with them. + */ @Override public void setRowData(int start, List values) { refreshSelectionModel(new ArrayList<>(listDataProvider.getList())); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java index 6deb0503e85..7e951ed56c1 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java @@ -1,42 +1,35 @@ package com.sap.sse.gwt.client.celltable; +import com.google.gwt.view.client.HasData; import com.google.gwt.view.client.ListDataProvider; import com.google.gwt.view.client.SetSelectionModel; import com.google.gwt.view.client.SingleSelectionModel; /** * The interface {@link RefreshableSelectionModel} unifies the refresh behavior of SelectionModels e.g. of a - * {@link SingleSelectionModel}. To refresh this SelectionModel you can call the - * {@link RefreshableSelectionModel#refreshSelectionModel(Iterable)} method and this method will refresh the - * SelectionModel. The refresh should be triggered after the {@link ListDataProvider} was updated. + * {@link SingleSelectionModel}. This {@link RefreshableSelectionModel selectionmodel} will automatically refresh the + * selection, when there are changes on the {@link ListDataProvider}. Therefore the {@link RefreshableSelectionModel + * selectionmodel} is a listener of the {@link ListDataProvider} and implements the {@link HasData}-interface. *

    - * - * TODO Lukas, please enhance this comment to explain how this re-establishes the invariant of same elements in - * SetSelectionModel.getSelectedElements and DataProvider + * All objects from the current selection that {@link EntityIdentityComparator#representSameEntity(Object, Object) + * represent the same entity} as an object from {@link ListDataProvider} will be reselected. All others are de-selected. + * That means a selected object is not contained in {@link ListDataProvider#getList()} wouldn't be selected anymore. If + * an element is reselected the element will be replaced with new version of it. + *

    + * If this selection model has no {@link EntityIdentityComparator} set, this method will use the {@link #equals(Object)} + * method to compare. + *

    + * When the selection is refreshed this {@link RefreshableSelectionModel selectionmodel} triggers a + * {@link SelectionChangeEvent.Handler#onSelectionChange(SelectionChangeEvent) onSelectionChangedEvent} using + * {@link AbstractSelectionModel#fireEvent(com.google.gwt.event.shared.GwtEvent)}. * * @author D064976 */ -public interface RefreshableSelectionModel extends SetSelectionModel { +public interface RefreshableSelectionModel extends SetSelectionModel, HasData { /** * @return Returns an instance of {@link EntityIdentityComparator}, to compare objects from the type * <T>. */ public EntityIdentityComparator getEntityIdentityComparator(); - - /** - * Refreshes the {@link RefreshableSelectionModel} with the newObjects. All objects from the current - * selection that {@link EntityIdentityComparator#representSameEntity(Object, Object) represent the same entity} as - * an object from newObjects will be reselected. All others are de-selected. That means a selected - * object is not contained in newObjects the object wouldn't be selected anymore. If this selection - * model has no {@link EntityIdentityComparator} set, this method will use the {@link #equals(Object)} method to - * compare. - *

    - * - * When the selection is refreshed this method triggers a - * {@link SelectionChangeEvent.Handler#onSelectionChange(SelectionChangeEvent) onSelectionChangedEvent} using - * {@link AbstractSelectionModel#fireEvent(com.google.gwt.event.shared.GwtEvent)}. - */ - // TODO / FIXME delete this method and describe interaction - // public void refreshSelectionModel(Iterable newObjects); -} +} \ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index 34c610fd05d..c9c845e8cb9 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -13,11 +13,20 @@ import com.google.gwt.view.client.SingleSelectionModel; import com.google.gwt.view.client.RangeChangeEvent.Handler; /** - * TODO Lukas: Add Javadoc + * This {@link RefreshableSingleSelectionModel} implements the {@link RefreshableSelectionModel} interface. So it + * register it self as a display on the {@link ListDataProvider} and reacts on the changes of {@link ListDataProvider}. + * When the {@link ListDataProvider} is changed this {@link RefreshableSingleSelectionModel selectionmodel} will refresh + * the selection according to the {@link ListDataProvider} changes. To make this class work correct it is very important + * to set the {@link ListDataProvider}, otherwise it won´t work. + *

    + * For more details on the update process read the {@link RefreshableSelectionModel} Javadoc and see the methods + * {@link RefreshableSingleSelectionModel#refreshSelectionModel(Iterable)} and + * {@link RefreshableSingleSelectionModel#setRowData(int, List)}. * - * * TODO /FIXME Describe interaction with ListDataProvider + * @author D064976 + * @param + * the type of entries */ - public class RefreshableSingleSelectionModel extends SingleSelectionModel implements RefreshableSelectionModel, HasData { private final EntityIdentityComparator comp; private boolean dontcheckSelectionState = false; @@ -26,6 +35,9 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel /** * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects + * @param listDataProvider + * {@link ListDataProvider} to add this {@link RefreshableSingleSelectionModel selectionmodel} as an + * display on {@link ListDataProvider} */ public RefreshableSingleSelectionModel(EntityIdentityComparator comp, ListDataProvider listDataProvider) { super(); @@ -39,6 +51,9 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel * {@link ProvidesKey} for the super class constructor * @param comp * {@link EntityIdentityComparator} to compare the identity of the objects + * @param listDataProvider + * {@link ListDataProvider} to add this {@link RefreshableSingleSelectionModel selectionmodel} as an + * display on {@link ListDataProvider} */ public RefreshableSingleSelectionModel(ProvidesKey keyProvider, EntityIdentityComparator comp, ListDataProvider listDataProvider) { super(keyProvider); @@ -47,11 +62,21 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel this.listDataProvider.addDataDisplay(this); } + /** + * @return the {@link EntityIdentityComparator} for the {@link RefreshableSingleSelectionModel}. If the + * {@link EntityIdentityComparator} is not set this method will return null. + */ @Override public EntityIdentityComparator getEntityIdentityComparator() { return comp; } + /** + * Checks the old selection state of the object. If it was selected before, the old version will be replaced with the + * new one. In all other cases this method behave same as super.setSelected(T item, boolean selected). + *

    + * When the {@link EntityIdentityComparator} is null this method also behaves like the super method + */ @Override public void setSelected(T item, boolean selected) { if (comp == null || dontcheckSelectionState || item == null || getSelectedObject() == null) { @@ -65,8 +90,25 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel } } } - - private void refreshSelectionModel(Iterable newObjects) { + + /** + * Refreshes the {@link RefreshableSingleSelectionModel} with the newObjects. If the current selected + * object {@link EntityIdentityComparator#representSameEntity(Object, Object) represent the same entity} as an + * object from newObjects it will be reselected. All others are de-selected. That means a selected + * object is not contained in newObjects the object wouldn't be selected anymore. If this selection + * model has no {@link EntityIdentityComparator} set, this method will use the {@link #equals(Object)} method to + * compare. If an object is reselected it will be replaced with the new version of it. + *

    + * + * When the selection is refreshed this method triggers a + * {@link SelectionChangeEvent.Handler#onSelectionChange(SelectionChangeEvent) onSelectionChangedEvent} using + * {@link AbstractSelectionModel#fireEvent(com.google.gwt.event.shared.GwtEvent)}. + * + * @param newObjects + * the new objects to refresh the {@link RefreshableSingleSelectionModel selectionmodel} + */ + private void refreshSelectionModel(Iterable newObjects) { + // avoid a new selection state check in setSelected dontcheckSelectionState = true; final T selected = getSelectedObject(); if (selected != null) { @@ -83,7 +125,10 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel dontcheckSelectionState = false; } - + /** + * This method is called when the {@link ListDataProvider} has new elements. It takes the new elements and refreshes + * the {@link RefreshableSingleSelectionModel selectionmodel} with them. + */ @Override public void setRowData(int start, List values) { refreshSelectionModel(new ArrayList<>(listDataProvider.getList())); From 77c035ec5f68b179917dac7a8f98f57a1a028571 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 9 Dec 2015 10:43:22 +0100 Subject: [PATCH 136/306] newSelectionModel: added Javadoc --- .../gwt/ui/client/shared/controls/FlushableCellTable.java | 8 ++++++++ .../FlushableSortedCellTableWithStylableHeaders.java | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableCellTable.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableCellTable.java index 65e77cb8f72..d5e48fb4d29 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableCellTable.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableCellTable.java @@ -4,6 +4,14 @@ import com.google.gwt.user.cellview.client.CellTable; import com.google.gwt.user.client.ui.Widget; import com.google.gwt.view.client.ProvidesKey; +/** + * This {@link FlushableCellTable} provides the {@link FlushableCellTable#flush()}-method for the + * {@link SelectionCheckboxColumn}. So the {@link SelectionCheckboxColumn} can ensure that the selection state is + * displayed correct. + * + * @author D064976 + * @param + */ public class FlushableCellTable extends CellTable implements Flushable { public FlushableCellTable() { super(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableSortedCellTableWithStylableHeaders.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableSortedCellTableWithStylableHeaders.java index 16243fe9b74..1cb9b545917 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableSortedCellTableWithStylableHeaders.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/FlushableSortedCellTableWithStylableHeaders.java @@ -3,6 +3,14 @@ package com.sap.sailing.gwt.ui.client.shared.controls; import com.sap.sailing.gwt.ui.leaderboard.LeaderboardTableResources; import com.sap.sailing.gwt.ui.leaderboard.SortedCellTableWithStylableHeaders; +/** + * This {@link FlushableSortedCellTableWithStylableHeaders} provides the + * {@link FlushableSortedCellTableWithStylableHeaders#flush()}-method for the {@link SelectionCheckboxColumn}. So the + * {@link SelectionCheckboxColumn} can ensure that the selection state is displayed correct. + * + * @author D064976 + * @param + */ public class FlushableSortedCellTableWithStylableHeaders extends SortedCellTableWithStylableHeaders implements Flushable { public FlushableSortedCellTableWithStylableHeaders(int pageSize, LeaderboardTableResources resources) { From 4cd717770beff453fc480e9c485d9d57cafc115a Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Wed, 9 Dec 2015 10:59:19 +0100 Subject: [PATCH 137/306] newSelectionModel: added javadoc --- .../sse/gwt/client/celltable/RefreshableSelectionModel.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java index 7e951ed56c1..ee8a50c916b 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSelectionModel.java @@ -2,6 +2,7 @@ package com.sap.sse.gwt.client.celltable; import com.google.gwt.view.client.HasData; import com.google.gwt.view.client.ListDataProvider; +import com.google.gwt.view.client.SelectionChangeEvent.Handler; import com.google.gwt.view.client.SetSelectionModel; import com.google.gwt.view.client.SingleSelectionModel; @@ -22,6 +23,10 @@ import com.google.gwt.view.client.SingleSelectionModel; * When the selection is refreshed this {@link RefreshableSelectionModel selectionmodel} triggers a * {@link SelectionChangeEvent.Handler#onSelectionChange(SelectionChangeEvent) onSelectionChangedEvent} using * {@link AbstractSelectionModel#fireEvent(com.google.gwt.event.shared.GwtEvent)}. + *

    + * When you use this {@link RefreshableSelectionModel selectionmodel} make sure that you don't modify + * {@link ListDataProvider} in {@link Handler#onSelectionChange(com.google.gwt.view.client.SelectionChangeEvent) + * onSelectionChange()} (it causes a stackoverflow). * * @author D064976 */ From 743e3d81991d41d928dc39a17bf1761c5a019265 Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Wed, 9 Dec 2015 14:36:55 +0100 Subject: [PATCH 138/306] improved performance of startline advantage by wind widget --- .../StartlineAdvantagesByWindComponent.java | 2 +- .../StartlineAdvantagesOnLineChart.java | 144 +++++------------- .../gwt/client/util/HighchartsUtil.java | 37 +++++ .../StartlineAdvantagesByWindCalculator.java | 47 ++++-- ...artlineAdvantagesWithMaxAndAverageDTO.java | 5 +- .../gwt/highcharts/client/Series.java | 35 ++++- 6 files changed, 140 insertions(+), 130 deletions(-) create mode 100644 java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/util/HighchartsUtil.java diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java index 9601d98b5fa..e5f619437e2 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesByWindComponent.java @@ -74,7 +74,7 @@ public class StartlineAdvantagesByWindComponent extends Composite implements Has logger.log(Level.INFO, "Received StartlineAdvantagesWithMaxAndAverageDTO"); if (result != null && result.maximum != null && result.average != null) { logger.log(Level.INFO, "Updating UI with StartlineAdvantagesWithMaxAndAverageDTO"); - startlineAdvantagesOnLineChart.setStartlineAdvantages(result.advantages); + startlineAdvantagesOnLineChart.setStartlineAdvantagesAndConfidences(result.distanceToRCBoatToStartlineAdvantage, result.distanceToRCBoatToConfidence); advantageMaximumLiveAverage.setLiveValue(NumberFormat.getFormat("#0.0").format( result.maximum.doubleValue())); advantageMaximumLiveAverage.setAverageValue(NumberFormat.getFormat("#0.0").format( diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesOnLineChart.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesOnLineChart.java index 6c68e0f9e05..dec5d9f4edb 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesOnLineChart.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/startlineadvantage/StartlineAdvantagesOnLineChart.java @@ -1,17 +1,15 @@ package com.sap.sailing.dashboards.gwt.client.startlineadvantage; import java.util.Iterator; -import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; -import org.moxieapps.gwt.highcharts.client.Animation; -import org.moxieapps.gwt.highcharts.client.Animation.Easing; import org.moxieapps.gwt.highcharts.client.AxisTitle; import org.moxieapps.gwt.highcharts.client.Chart; import org.moxieapps.gwt.highcharts.client.Color; import org.moxieapps.gwt.highcharts.client.Credits; import org.moxieapps.gwt.highcharts.client.Legend; 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.Style; import org.moxieapps.gwt.highcharts.client.ToolTip; @@ -31,9 +29,8 @@ import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.dashboards.gwt.shared.dto.StartLineAdvantageDTO; +import com.sap.sailing.dashboards.gwt.client.util.HighchartsUtil; import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sse.common.Util.Pair; /** * @author Alexander Ries (D062114) @@ -63,28 +60,31 @@ public class StartlineAdvantagesOnLineChart extends Composite implements HasWidg private Chart chart; private Series series; - - private static final int POINT_ADDING_ANIMATION_DURATION_IN_MILLIS = 2000; + private static final Logger logger = Logger.getLogger(StartlineAdvantagesByWindComponent.class.getName()); public StartlineAdvantagesOnLineChart() { initWidget(uiBinder.createAndBindUi(this)); initAndAddChart(); setConfidenceColorBarLabelValues(); - //TODO Remove and centralize data retrieving + } + + public void setStartlineAdvantagesAndConfidences(Number [][] distanceToRCBoatToStartlineAdvantages, Number [][] distanceToRCBoatToConfidences) { + if (distanceToRCBoatToStartlineAdvantages != null) { + series.setPoints(distanceToRCBoatToStartlineAdvantages, true); + } + setConfidenceGradientColorToSeries(series, distanceToRCBoatToConfidences); + chart.redraw(); } private void initAndAddChart() { chart = initChart(); - /** - * Highcharts default chart element spacing is not consistent - * so there is a need to correct the right spacing - * of the chart to make it look visually right. - * */ - setCorrectChartSpacing(chart); + HighchartsUtil.correctSpacingOfChart(chart); setXAxisOfChart(chart); setYAxisOfChart(chart); series = createSeries(chart); + chart.addSeries(series); chartContainer.add(chart); + HighchartsUtil.setSizeToMatchContainerDelayed(chart); } private Chart initChart() { @@ -97,22 +97,16 @@ public class StartlineAdvantagesOnLineChart extends Composite implements HasWidg .setBackgroundColor("#FFFFFF") .setBorderColor("#FFFFFF") .setPlotShadow(false) + .setWidth100() + .setHeight100() .setPlotBorderColor("#FFFFFF") .setOption("/exporting/enabled", false) .setShadow(false) .setStyle(new Style().setPosition("absolute").setTop("0px").setBottom("0px").setLeft("0px").setRight("0px")) .setAnimation(true); - chart.setSize("100%", "100%"); - chart.setSizeToMatchContainer(); return chart; } - private void setCorrectChartSpacing(Chart chart){ - chart.setSpacingTop(17) - .setSpacingBottom(13) - .setSpacingRight(35); - } - private void setXAxisOfChart(Chart chart){ chart.getXAxis() .setAxisTitle(new AxisTitle().setText(StringMessages.INSTANCE.dashboardDistanceToRCBoat()).setStyle(new Style().setFontFamily("Open Sans") @@ -156,10 +150,9 @@ public class StartlineAdvantagesOnLineChart extends Composite implements HasWidg Series series = chart .createSeries() .setName(null) - .setPlotOptions( - new AreaPlotOptions().setDashStyle(DashStyle.SOLID).setLineWidth(1) + .setPlotOptions(new AreaPlotOptions().setDashStyle(DashStyle.SOLID).setLineWidth(1) .setMarker(new Marker().setEnabled(false)).setShadow(false).setHoverStateEnabled(false) - .setLineColor("#FFFFFF").setFillColor("#FFFFFF")); + .setLineColor("white").setFillColor("white")); return series; } @@ -170,71 +163,18 @@ public class StartlineAdvantagesOnLineChart extends Composite implements HasWidg } private String getAxisLabelValueForLabelData(double labelData) { - String result = String.valueOf(labelData); - com.sap.sse.common.Util.Pair firstAndLastXValue = getFirstnAndLastXValueOfSeries(); - if (firstAndLastXValue != null && firstAndLastXValue.getA() != null && firstAndLastXValue.getB() != null) { - chart.getXAxis().setTickInterval(firstAndLastXValue.getB()/2); - if (labelData == firstAndLastXValue.getA().doubleValue()) { - result = StringMessages.INSTANCE.dashboardRCBoat(); - } else if (labelData == firstAndLastXValue.getB().doubleValue()) { - result = StringMessages.INSTANCE.dashboardPinEnd(); - } - } - return result; - } - - private com.sap.sse.common.Util.Pair getFirstnAndLastXValueOfSeries(){ - if(series != null && - series.getPoints() != null && - series.getPoints().length > 1){ - Double firstValue = new Double(series.getPoints()[0].getX().doubleValue()); - Double lastValue = new Double(series.getPoints()[series.getPoints().length-1].getX().doubleValue()); - com.sap.sse.common.Util.Pair firstAndLastXValue = new Pair(firstValue, lastValue); - return firstAndLastXValue; - }else{ - return null; + if(labelData != 0) { + return labelData+""; + }else { + return "RC Boat"; } } - public void setStartlineAdvantages(List startlineAdvantages) { - if (startlineAdvantages != null) { - if (series.getPoints() != null && series.getPoints().length > 0) { - updateStartlineAdvantages(startlineAdvantages, series); - } else { - addStartlineAdvantages(startlineAdvantages, series); - chart.addSeries(series, true, new Animation().setDuration(POINT_ADDING_ANIMATION_DURATION_IN_MILLIS).setEasing(Easing.SWING)); - } - } - chart.setSizeToMatchContainer(); - } - - private void updateStartlineAdvantages(List startlineAdvantages, Series series) { - removeAllSeriesPoints(chart.getSeries()[0]); - addStartlineAdvantages(startlineAdvantages, series); - } - - private void removeAllSeriesPoints(Series series) { - for (Point point : series.getPoints()) { - series.removePoint(point); - } - } - - private void addStartlineAdvantages(List startlineAdvantages, Series series){ - //TODO Build this with forEach consumer as soon as the bundle uses GWT 2.8 with Java 8 Support. - for (StartLineAdvantageDTO startlineAdvantage : startlineAdvantages) { - Point point = new Point(startlineAdvantage.distanceToRCBoatInMeters, startlineAdvantage.startLineAdvantage); - series.addPoint(point, false, false, false); - } - setConfidenceGradientColorToSeries(series, startlineAdvantages); - chart.redraw(); - } - - private void setConfidenceGradientColorToSeries(Series series, List startlineAdvantages) { - Color confidenceGradientColor = ConfidenceColorCalculator.getColorWithConfidenceGradients(startlineAdvantages); - series.setPlotOptions( - new AreaPlotOptions().setDashStyle(DashStyle.SOLID).setLineWidth(1) - .setMarker(new Marker().setEnabled(false)).setShadow(false).setHoverStateEnabled(false) - .setLineColor("#FFFFFF").setFillColor(confidenceGradientColor)); + private void setConfidenceGradientColorToSeries(Series series, Number [][] distanceToRCBoatToConfidences) { + Color confidenceGradientColor = ConfidenceColorCalculator.getColorWithConfidenceGradients(distanceToRCBoatToConfidences); + String fillColor = confidenceGradientColor.getOptions().toString(); + logger.log(Level.INFO, fillColor); + series.updateSeriesFillColor(fillColor); } @Override @@ -261,26 +201,21 @@ public class StartlineAdvantagesOnLineChart extends Composite implements HasWidg private static final String LOW_CONFIDENCE_COLOR_AS_HEX = "#43cea2"; private static final String HIGHT_CONFIDENCE_COLOR_AS_HEX = "#008FFF"; - public static Color getColorWithConfidenceGradients(List startlineAdvantages) { + public static Color getColorWithConfidenceGradients(Number [][] distanceToRCBoatToConfidences) { Color color = new Color().setLinearGradient(200, 0, 0, 0); - double lineLenght = getBiggestDistanceToRCBoat(startlineAdvantages); - //TODO Need to find a prettier way of at adding color stops - double lastConfidence = 2; - for (StartLineAdvantageDTO startlineAdvantageDTO : startlineAdvantages) { - if (lastConfidence != startlineAdvantageDTO.confidence) { - lastConfidence = startlineAdvantageDTO.confidence; - double gradientPosition = getGradientStopPositionFromStartlineLenghtAndDistanceToRCBoat(lineLenght, - startlineAdvantageDTO.distanceToRCBoatInMeters); - color.addColorStop(gradientPosition, getColorAsHexStringFromConfidence(startlineAdvantageDTO.confidence)); + int previousIndex = 0; + for (int i = 0; i < distanceToRCBoatToConfidences.length; i++) { + double previousConfidence = distanceToRCBoatToConfidences[previousIndex][1].doubleValue(); + double currentConfidence = distanceToRCBoatToConfidences[i][1].doubleValue(); + if (previousConfidence != currentConfidence || i == 0) { + logger.log(Level.INFO, previousConfidence+" "+currentConfidence); + color.addColorStop(i, getColorAsHexStringFromConfidence(distanceToRCBoatToConfidences[i][0].doubleValue())); } + previousIndex++; } return color; } - private static double getGradientStopPositionFromStartlineLenghtAndDistanceToRCBoat(double startlineLenght, double distanceToRCBoat) { - return distanceToRCBoat/startlineLenght; - } - private static String getColorAsHexStringFromConfidence(double position) { if(position > 0.6) { return HIGHT_CONFIDENCE_COLOR_AS_HEX; @@ -288,10 +223,5 @@ public class StartlineAdvantagesOnLineChart extends Composite implements HasWidg return LOW_CONFIDENCE_COLOR_AS_HEX; } } - - private static double getBiggestDistanceToRCBoat(List startlineAdvantages) { - StartLineAdvantageDTO startlineAdvantageDTO = startlineAdvantages.get(startlineAdvantages.size()-1); - return startlineAdvantageDTO.distanceToRCBoatInMeters; - } } } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/util/HighchartsUtil.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/util/HighchartsUtil.java new file mode 100644 index 00000000000..dbd59b31765 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/util/HighchartsUtil.java @@ -0,0 +1,37 @@ +package com.sap.sailing.dashboards.gwt.client.util; + +import org.moxieapps.gwt.highcharts.client.Chart; + +import com.google.gwt.user.client.Timer; + +/** + * @author Alexander Ries (D062114) + * + */ +public class HighchartsUtil { + + /** + * The {@link Chart} spacing is not consistent + * so there is a need to correct the top, bottom and right spacing + * of the chart to make it look visually right. + * */ + public static void correctSpacingOfChart(Chart chart) { + chart.setSpacingTop(17) + .setSpacingBottom(13) + .setSpacingRight(35); + } + + /** + * In cases where it is necessary to initialize a {@link Chart}, with dynamic size during the same browser loop, where it´s dynamic container is initialized, + * the {@link Chart.setSizeToMatchContainer()} method will not work. The solution to that problem is to call {@link Chart#setSizeToMatchContainer()} later, + * when the container of the chart is completely initialized and its size is available to the {@link Chart}. + * */ + public static void setSizeToMatchContainerDelayed(final Chart chart) { + Timer elapsedTimer = new Timer() { + public void run() { + chart.setSizeToMatchContainer(); + } + }; + elapsedTimer.schedule(500); + } +} diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/StartlineAdvantagesByWindCalculator.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/StartlineAdvantagesByWindCalculator.java index 0b625112564..806bd4bbf3c 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/StartlineAdvantagesByWindCalculator.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startlineadvantage/StartlineAdvantagesByWindCalculator.java @@ -41,20 +41,23 @@ public class StartlineAdvantagesByWindCalculator extends AbstracPreCalculationD if (trackedRace != null) { retrieveDataForCalculation(trackedRace, dashboardDispatchContext.getPolarDataService()); if (wind != null) { + Pair startlineAdvantagesAndConfidencesAsArray = null; if (isStartlineCompletelyUnderneathLaylines()) { logger.log(Level.INFO, "Startline is completely underneath laylines"); Pair advantagesRange = new Pair(0.0, startlineLenghtInMeters); - result.advantages = calculateStartlineAdvantagesUnderneathLaylinesInRange(advantagesRange); - double maximum = getMaximumAdvantageOfStartlineAdvantageDTOs(result.advantages); + List startlineAdvantages = calculateStartlineAdvantagesUnderneathLaylinesInRange(advantagesRange); + double maximum = getMaximumAdvantageOfStartlineAdvantageDTOs(startlineAdvantages); result.maximum = maximum; + startlineAdvantagesAndConfidencesAsArray = convertStartLineAdvantageDTOListToPointAndConfidenceArrays(startlineAdvantages); } else if (isStartlineCompletelyAboveLaylines()) { logger.log(Level.INFO, "Startline is completely above laylines"); Pair advantagesRange = new Pair(0.0, startlineLenghtInMeters); - result.advantages = calculatePolarBasedStartlineAdvantagesInRange(advantagesRange); - subtractMinimumOfAllStartlineAdvantages(result.advantages); - double maximum = getMaximumAdvantageOfStartlineAdvantageDTOs(result.advantages); + List startlineAdvantages = calculatePolarBasedStartlineAdvantagesInRange(advantagesRange); + subtractMinimumOfAllStartlineAdvantages(startlineAdvantages); + double maximum = getMaximumAdvantageOfStartlineAdvantageDTOs(startlineAdvantages); result.maximum = maximum; - subtractAgainstMaximumOfAllStartlineAdvantages(result.advantages, maximum); + subtractAgainstMaximumOfAllStartlineAdvantages(startlineAdvantages, maximum); + startlineAdvantagesAndConfidencesAsArray = convertStartLineAdvantageDTOListToPointAndConfidenceArrays(startlineAdvantages); } else { logger.log(Level.INFO, "Layline(s) cross startline"); Position intersectionOfRightLaylineAndStartline = getIntersectionOfRightLaylineAndStartline(); @@ -62,12 +65,17 @@ public class StartlineAdvantagesByWindCalculator extends AbstracPreCalculationD Pair polarBasedStartlineAdvatagesRange = getStartAndEndPointOfPolarBasedStartlineAdvatagesInDistancesToRCBoat( intersectionOfRightLaylineAndStartline, intersectionOfleftLaylineAndStartline); Pair pinEndStartlineAdvatagesRange = getPinEndStartlineAdvantagesRangeFromPolarAdvantagesRange(polarBasedStartlineAdvatagesRange); - result.advantages = calculatePolarBasedStartlineAdvantagesInRange(polarBasedStartlineAdvatagesRange); - subtractMinimumOfAllStartlineAdvantages(result.advantages); - double maximum = getMaximumAdvantageOfStartlineAdvantageDTOs(result.advantages); + List startlineAdvantages = calculatePolarBasedStartlineAdvantagesInRange(polarBasedStartlineAdvatagesRange); + subtractMinimumOfAllStartlineAdvantages(startlineAdvantages); + double maximum = getMaximumAdvantageOfStartlineAdvantageDTOs(startlineAdvantages); result.maximum = maximum; - subtractAgainstMaximumOfAllStartlineAdvantages(result.advantages, maximum); - addClosingZeroPointToMixedAdvantages(result.advantages, pinEndStartlineAdvatagesRange); + subtractAgainstMaximumOfAllStartlineAdvantages(startlineAdvantages, maximum); + addClosingZeroPointToMixedAdvantages(startlineAdvantages, pinEndStartlineAdvatagesRange); + startlineAdvantagesAndConfidencesAsArray = convertStartLineAdvantageDTOListToPointAndConfidenceArrays(startlineAdvantages); + } + if (startlineAdvantagesAndConfidencesAsArray != null) { + result.distanceToRCBoatToStartlineAdvantage = startlineAdvantagesAndConfidencesAsArray.getA(); + result.distanceToRCBoatToConfidence = startlineAdvantagesAndConfidencesAsArray.getB(); } } } else { @@ -248,6 +256,23 @@ public class StartlineAdvantagesByWindCalculator extends AbstracPreCalculationD return result; } + private Pair convertStartLineAdvantageDTOListToPointAndConfidenceArrays(List startLineAdvantageDTOList) { + Pair result = null; + if(startLineAdvantageDTOList != null && startLineAdvantageDTOList.size() > 0) { + Number[][] distanceToRCBoatToStartlineAdvantage = new Number[startLineAdvantageDTOList.size()][2]; + Number[][] distanceToRCBoatToConfidence = new Number[startLineAdvantageDTOList.size()][2]; + for(int i = 0; i < startLineAdvantageDTOList.size(); i++) { + StartLineAdvantageDTO startLineAdvantageDTO = startLineAdvantageDTOList.get(i); + distanceToRCBoatToStartlineAdvantage[i][0] = startLineAdvantageDTO.distanceToRCBoatInMeters; + distanceToRCBoatToStartlineAdvantage[i][1] = startLineAdvantageDTO.startLineAdvantage; + distanceToRCBoatToConfidence[i][0] = startLineAdvantageDTO.distanceToRCBoatInMeters; + distanceToRCBoatToConfidence[i][1] = startLineAdvantageDTO.confidence; + } + result = new Pair(distanceToRCBoatToStartlineAdvantage, distanceToRCBoatToConfidence); + } + return result; + } + private List calculatePolarBasedStartlineAdvantagesInRange(Pair rangePinEndStartlineAdvantage) { List result = new ArrayList(); if (rangePinEndStartlineAdvantage != null) { diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartlineAdvantagesWithMaxAndAverageDTO.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartlineAdvantagesWithMaxAndAverageDTO.java index 4dab1d3f485..52ab09d94ba 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartlineAdvantagesWithMaxAndAverageDTO.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartlineAdvantagesWithMaxAndAverageDTO.java @@ -1,7 +1,5 @@ package com.sap.sailing.dashboards.gwt.shared.dto; -import java.util.List; - import com.sap.sailing.gwt.dispatch.client.Result; /** @@ -11,7 +9,8 @@ import com.sap.sailing.gwt.dispatch.client.Result; public class StartlineAdvantagesWithMaxAndAverageDTO extends AverageDTO implements Result { public Double maximum; - public List advantages; + public Number [][] distanceToRCBoatToStartlineAdvantage; + public Number [][] distanceToRCBoatToConfidence; public StartlineAdvantagesWithMaxAndAverageDTO(){} } diff --git a/java/org.moxieapps.gwt.highcharts/src/org/moxieapps/gwt/highcharts/client/Series.java b/java/org.moxieapps.gwt.highcharts/src/org/moxieapps/gwt/highcharts/client/Series.java index a7749f03a8a..d60d1c9fd4e 100755 --- a/java/org.moxieapps.gwt.highcharts/src/org/moxieapps/gwt/highcharts/client/Series.java +++ b/java/org.moxieapps.gwt.highcharts/src/org/moxieapps/gwt/highcharts/client/Series.java @@ -16,16 +16,23 @@ package org.moxieapps.gwt.highcharts.client; -import com.google.gwt.core.client.JavaScriptObject; -import com.google.gwt.core.client.JsArray; -import com.google.gwt.dom.client.Document; -import com.google.gwt.json.client.*; -import org.moxieapps.gwt.highcharts.client.plotOptions.PlotOptions; - import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; +import org.moxieapps.gwt.highcharts.client.plotOptions.PlotOptions; + +import com.google.gwt.core.client.JavaScriptObject; +import com.google.gwt.core.client.JsArray; +import com.google.gwt.core.client.JsonUtils; +import com.google.gwt.dom.client.Document; +import com.google.gwt.json.client.JSONArray; +import com.google.gwt.json.client.JSONNull; +import com.google.gwt.json.client.JSONNumber; +import com.google.gwt.json.client.JSONObject; +import com.google.gwt.json.client.JSONString; +import com.google.gwt.json.client.JSONValue; + /** * Manages a data series (and its options) that can then be added to a {@link org.moxieapps.gwt.highcharts.client.Chart}. As an extension * of {@link org.moxieapps.gwt.highcharts.client.Configurable} each series instance can also optionally have configuration options set on it. @@ -1230,13 +1237,25 @@ public class Series extends Configurable { }-*/; public void updateThreshold(String threshold) { - updateNativeWay(this.getNativeSeries(), threshold); + updateThresHoldNative(this.getNativeSeries(), threshold); + } + + public void updateSeriesFillColor(String fillColor) { + JavaScriptObject fillColorObject = JsonUtils.safeEval(fillColor); + updateFillColorNative(this.getNativeSeries(), fillColorObject); } - public static native void updateNativeWay(JavaScriptObject series, String threshold) /*-{ + public static native void updateThresHoldNative(JavaScriptObject series, String threshold) /*-{ series.update({ threshold: threshold, borderColor: '#FF0000' }) }-*/; + + public static native void updateFillColorNative(JavaScriptObject series, JavaScriptObject fillColorValue) /*-{ + series.update({ + fillColor: fillColorValue + }) + }-*/; + } From 5ce9fa6b0ffd0cf042b2cbccf997f3c875f20f11 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 9 Dec 2015 14:45:24 +0100 Subject: [PATCH 139/306] removed version specificity for datamining.annotations dependency in com.sap.sse.commong Change-Id: Ida5a872fbb7d66e36bd08b3e7e39d9476cf89285 --- java/com.sap.sse.common/META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sse.common/META-INF/MANIFEST.MF b/java/com.sap.sse.common/META-INF/MANIFEST.MF index 81dcd9a9a05..d8f1e10697a 100644 --- a/java/com.sap.sse.common/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.common/META-INF/MANIFEST.MF @@ -15,5 +15,5 @@ Export-Package: com.sap.sse.common, com.sap.sse.common.settings, com.sap.sse.common.util Require-Bundle: org.json.simple;bundle-version="1.1.0", - com.sap.sse.datamining.annotations;bundle-version="1.0.0" + com.sap.sse.datamining.annotations Bundle-ActivationPolicy: lazy From 1e951ba6cb47f20799a2bcc511a9211bfe55fe1c Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Wed, 9 Dec 2015 15:10:34 +0100 Subject: [PATCH 140/306] increased speed of vertical wind chart --- .../sailing/dashboards/gwt/client/util/HighchartsUtil.java | 2 +- .../dashboards/gwt/client/windchart/VerticalWindChart.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/util/HighchartsUtil.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/util/HighchartsUtil.java index dbd59b31765..d1bbd46a05c 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/util/HighchartsUtil.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/util/HighchartsUtil.java @@ -22,7 +22,7 @@ public class HighchartsUtil { } /** - * In cases where it is necessary to initialize a {@link Chart}, with dynamic size during the same browser loop, where it´s dynamic container is initialized, + * In cases where it is necessary to initialize a {@link Chart} with dynamic size during the same browser loop, where it´s dynamic container is initialized, * the {@link Chart.setSizeToMatchContainer()} method will not work. The solution to that problem is to call {@link Chart#setSizeToMatchContainer()} later, * when the container of the chart is completely initialized and its size is available to the {@link Chart}. * */ diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java index 58eb388f314..484b1a424d4 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java @@ -40,6 +40,7 @@ import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FocusPanel; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.dashboards.gwt.client.util.HighchartsUtil; /** * The class represents a wind chart that is displayed vertically. Because it is meant to be used to display wind fixes, @@ -114,6 +115,7 @@ public class VerticalWindChart extends Composite implements HasWidgets { positiveSeriesColorAsHex = positiveFillColor; negativeSeriesColorAsHex = negativeFillColor; verticalWindChartClickListeners = new ArrayList(); + HighchartsUtil.setSizeToMatchContainerDelayed(verticalWindChart); } private void setChartOptions() { @@ -207,7 +209,6 @@ public class VerticalWindChart extends Composite implements HasWidgets { } verticalWindChartSeries.updateThreshold("" + average); adaptVerticalWindChartExtemes(); - verticalWindChart.setSizeToMatchContainer(); } }); } @@ -229,7 +230,7 @@ public class VerticalWindChart extends Composite implements HasWidgets { private void setXAxisExtremesForSeriesPointRangeIsBiggerThanChartDisplayIntervall() { long maximumExtremeValueInMillis = getLastPointOfVerticalWindChartSeries().getX().longValue(); long minimumExtremeValueInMillis = maximumExtremeValueInMillis - chartIntervallinMinutes * 60 * 1000; - verticalWindChart.getXAxis().setExtremes(minimumExtremeValueInMillis, maximumExtremeValueInMillis, true, true); + verticalWindChart.getXAxis().setExtremes(minimumExtremeValueInMillis, maximumExtremeValueInMillis, true, false); } From 9fa6b3b2bec01b366068e4a6771d53f5529df01a Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Wed, 9 Dec 2015 15:19:34 +0100 Subject: [PATCH 141/306] removed scheduler from vertical windchart --- .../client/windchart/VerticalWindChart.java | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java index 484b1a424d4..9a2613e0481 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java @@ -28,8 +28,6 @@ import org.moxieapps.gwt.highcharts.client.plotOptions.BarPlotOptions; import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; 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.event.dom.client.ClickEvent; import com.google.gwt.resources.client.CssResource; import com.google.gwt.uibinder.client.UiBinder; @@ -193,24 +191,18 @@ public class VerticalWindChart extends Composite implements HasWidgets { * {@link #setXAxisExtremesForSeriesPointRangeIsBiggerThanChartDisplayIntervall()}. * */ public void addPointsToSeriesWithAverage(final Point[] points, final double average) { - Scheduler.get().scheduleDeferred(new ScheduledCommand() { - - @Override - public void execute() { - if (verticalWindChartSeries == null) { - initVerticalWindChartSeries(); - verticalWindChartSeries.setPoints(points, true); - logger.log(Level.INFO, "Set Points"); - } else { - for (Point point : points) { - verticalWindChartSeries.addPoint(point, true, false, false); - logger.log(Level.INFO, "Add Point"); - } - } - verticalWindChartSeries.updateThreshold("" + average); - adaptVerticalWindChartExtemes(); + if (verticalWindChartSeries == null) { + initVerticalWindChartSeries(); + verticalWindChartSeries.setPoints(points, true); + logger.log(Level.INFO, "Set Points"); + } else { + for (Point point : points) { + verticalWindChartSeries.addPoint(point, true, false, false); + logger.log(Level.INFO, "Add Point"); } - }); + } + verticalWindChartSeries.updateThreshold("" + average); + adaptVerticalWindChartExtemes(); } /** @@ -271,13 +263,8 @@ public class VerticalWindChart extends Composite implements HasWidgets { } else { chartIntervallinMinutes = SMALL_DISPLAY_INTERVALL_IN_MINUTES; } - Scheduler.get().scheduleFinally(new ScheduledCommand() { - - @Override - public void execute() { - adaptVerticalWindChartExtemes(); - notifyVerticalWindChartClickListeners(chartIntervallinMinutes); - }}); + adaptVerticalWindChartExtemes(); + notifyVerticalWindChartClickListeners(chartIntervallinMinutes); } @Override From dbc1090206443e65856ccb90dea601678330f261 Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Wed, 9 Dec 2015 16:00:12 +0100 Subject: [PATCH 142/306] Added the possibility to process the results of AutomaticBatchingDispatch deferred --- .../batching/AutomaticBatchingDispatch.java | 18 ++++++- .../client/batching/DispatchCallStack.java | 35 ++++++++++--- .../client/batching/SplitScheduler.java | 52 +++++++++++++++++++ .../client/impl/DispatchSystemImpl.java | 12 ++++- 4 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/SplitScheduler.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/AutomaticBatchingDispatch.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/AutomaticBatchingDispatch.java index 59a28fa0249..05da5d64a75 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/AutomaticBatchingDispatch.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/AutomaticBatchingDispatch.java @@ -18,6 +18,8 @@ public class AutomaticBatchingDispatch implements D private DispatchCallStack currentCallstack; + private boolean processResultsScheduled; + /** * Creates a batching dispatch service using the given dispatch implementation. * @@ -28,7 +30,21 @@ public class AutomaticBatchingDispatch implements D * */ public AutomaticBatchingDispatch(DispatchAsync service) { + this(service, false); + } + + /** + * Creates a batching dispatch service using the given dispatch implementation. + * + * The underlying dispatch service is responsible for the context propagation. + * + * @param service + * the underlying service implementation to use + * + */ + public AutomaticBatchingDispatch(DispatchAsync service, boolean processResultsScheduled) { this.executionContext = service; + this.processResultsScheduled = processResultsScheduled; } /** @@ -73,7 +89,7 @@ public class AutomaticBatchingDispatch implements D @Override public void onSuccess(BatchResult result) { - callStackInExecution.processResult(result); + callStackInExecution.processResult(result, processResultsScheduled); } }); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/DispatchCallStack.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/DispatchCallStack.java index 2bec8747ffb..9f41dd26cf5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/DispatchCallStack.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/DispatchCallStack.java @@ -2,6 +2,7 @@ package com.sap.sailing.gwt.dispatch.client.batching; import java.util.LinkedList; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.user.client.rpc.AsyncCallback; import com.sap.sailing.gwt.dispatch.client.Action; import com.sap.sailing.gwt.dispatch.client.DispatchContext; @@ -70,22 +71,44 @@ public final class DispatchCallStack { * * @param batchResult */ - @SuppressWarnings("unchecked") - public void processResult(BatchResult batchResult) { + public void processResult(BatchResult batchResult, boolean processResultsScheduled) { assert (batchResult != null); int loop = 0; for (@SuppressWarnings("rawtypes") DispatchCall call : this.actionQueue) { Result result = batchResult.getResult(loop); + Throwable exception = batchResult.getException(loop); - if (result == null) { - Throwable exception = batchResult.getException(loop); - call.getCallback().onFailure(exception); + CallbackCommand callbackCommand = new CallbackCommand(call, result, exception); + if(processResultsScheduled) { + SplitScheduler.get().schedule(callbackCommand); } else { - call.getCallback().onSuccess(result); + callbackCommand.execute(); } loop++; } } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private class CallbackCommand implements ScheduledCommand { + private final DispatchCall call; + private final Result result; + private final Throwable exception; + public CallbackCommand(DispatchCall call, Result result, Throwable exception) { + this.call = call; + this.result = result; + this.exception = exception; + } + + @Override + public void execute() { + if (result == null) { + call.getCallback().onFailure(exception); + } else { + call.getCallback().onSuccess(result); + } + } + + } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/SplitScheduler.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/SplitScheduler.java new file mode 100644 index 00000000000..c92b460b3db --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/batching/SplitScheduler.java @@ -0,0 +1,52 @@ +package com.sap.sailing.gwt.dispatch.client.batching; + +import java.util.ArrayList; +import java.util.List; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; +import com.google.gwt.core.client.Scheduler.RepeatingCommand; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; + +public class SplitScheduler { + + private static final SplitScheduler INSTANCE = new SplitScheduler(); + + private List pendingCommands = new ArrayList(); + + private boolean scheduled = false; + + private final RepeatingCommand repeatingCommand = new RepeatingCommand() { + @Override + public boolean execute() { + ScheduledCommand commandToExecute = pendingCommands.remove(0); + + try { + commandToExecute.execute(); + } catch (Exception e) { + if(GWT.getUncaughtExceptionHandler() != null) { + GWT.getUncaughtExceptionHandler().onUncaughtException(e); + } + } + scheduled = !pendingCommands.isEmpty(); + return scheduled; + } + }; + + private SplitScheduler() { + } + + public static SplitScheduler get() { + return INSTANCE; + } + + public void schedule(ScheduledCommand command) { + pendingCommands.add(command); + + if(!scheduled) { + Scheduler.get().scheduleIncremental(repeatingCommand); + scheduled = true; + } + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/impl/DispatchSystemImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/impl/DispatchSystemImpl.java index 5bb331f988f..464bf283c52 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/impl/DispatchSystemImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/dispatch/client/impl/DispatchSystemImpl.java @@ -16,8 +16,16 @@ import com.sap.sailing.gwt.dispatch.client.rpcimpl.SimpleDispatch; public class DispatchSystemImpl implements DispatchSystem { private final SimpleDispatch simpleDispatch = new SimpleDispatch(null); - private final DispatchAsync dispatch = new CachingDispatch(new AutomaticBatchingDispatch( - simpleDispatch)); + private final DispatchAsync dispatch; + + public DispatchSystemImpl() { + this(false); + } + + public DispatchSystemImpl(boolean processResultsScheduled) { + dispatch = new CachingDispatch(new AutomaticBatchingDispatch( + simpleDispatch, processResultsScheduled)); + } @Override public > void execute(final A action, final AsyncCallback callback) { From 0f96f70f8a7a0ed1d795fc323e20082096493ccb Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 9 Dec 2015 21:34:18 +0100 Subject: [PATCH 143/306] removed ess40 gateway bundle from java/pom.xml Change-Id: Ibf3cc050bd5e8036ec1c141934003234e22f87dc --- java/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/java/pom.xml b/java/pom.xml index fc283d0da09..bb6019ccf4f 100755 --- a/java/pom.xml +++ b/java/pom.xml @@ -57,7 +57,6 @@ com.sap.sailing.expeditionconnector com.sap.sailing.server com.sap.sailing.server.gateway - com.sap.sailing.server.gateway.ess40 com.sap.sailing.server.gateway.serialization com.sap.sailing.server.gateway.serialization.shared.android com.sap.sailing.www From 265f30a10587202a58d46e554f15c6b31bb8d0e8 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Thu, 10 Dec 2015 10:52:45 +0100 Subject: [PATCH 144/306] newSelectionModel: new trial to fix problems with LeaderboardConfigPanels --- .../AbstractLeaderboardConfigPanel.java | 87 ++++++++++--------- .../adminconsole/LeaderboardConfigPanel.java | 30 ++++--- ...martphoneTrackingEventManagementPanel.java | 11 ++- .../controls/SelectionCheckboxColumn.java | 2 + 4 files changed, 69 insertions(+), 61 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index 77e713a90a9..2d0da3dabde 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -10,6 +10,7 @@ import java.util.Set; 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.event.shared.HandlerRegistration; import com.google.gwt.user.cellview.client.CellTable; import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; import com.google.gwt.user.client.Window; @@ -24,7 +25,6 @@ import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.VerticalPanel; 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.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaNameAndRaceName; import com.sap.sailing.domain.common.dto.FleetDTO; @@ -52,7 +52,6 @@ import com.sap.sse.gwt.client.async.MarkedAsyncCallback; import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; -import com.sap.sse.gwt.client.celltable.RefreshableSingleSelectionModel; import com.sap.sse.gwt.client.dialog.DataEntryDialog.DialogCallback; import com.sap.sse.gwt.client.panels.LabeledAbstractFilterablePanel; @@ -85,11 +84,12 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement protected List availableLeaderboardList; - protected final RefreshableMultiSelectionModel refreshableLeaderboardSelectionModel; - - protected final RefreshableSingleSelectionModel raceSingleSelectionModel; - protected boolean reactOnSelectionChangeEvent; + protected final RefreshableMultiSelectionModel leaderboardSelectionModel; + protected final RefreshableSelectionModel refreshableTrackedRaceSelectionModel; + protected final SelectionChangeEvent.Handler trackedRaceListHandler; + protected HandlerRegistration trackedRaceListHandlerRegistration; + private final LeaderboardsRefresher leaderboardsRefresher; public static class RaceColumnDTOAndFleetDTOWithNameBasedEquality extends Util.Pair { @@ -144,7 +144,6 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement mainPanel = new VerticalPanel(); mainPanel.setWidth("100%"); this.setWidget(mainPanel); - reactOnSelectionChangeEvent = true; //Create leaderboards list and functionality CaptionPanel leaderboardsCaptionPanel = new CaptionPanel(stringMessages.leaderboards()); @@ -178,10 +177,10 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement leaderboardTable.ensureDebugId("AvailableLeaderboardsTable"); addColumnsToLeaderboardTableAndSetSelectionModel(leaderboardTable, tableRes, leaderboardList); @SuppressWarnings("unchecked") - RefreshableMultiSelectionModel refreshableMultiSelectionModel = (RefreshableMultiSelectionModel) leaderboardTable.getSelectionModel(); - refreshableLeaderboardSelectionModel = refreshableMultiSelectionModel; + RefreshableMultiSelectionModel multiSelectionModel = (RefreshableMultiSelectionModel) leaderboardTable.getSelectionModel(); + leaderboardSelectionModel = multiSelectionModel; leaderboardTable.setWidth("100%"); - refreshableLeaderboardSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { + leaderboardSelectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { public void onSelectionChange(SelectionChangeEvent event) { leaderboardSelectionChanged(); raceColumnTable.setSelectedLeaderboardName(getSelectedLeaderboardName()); @@ -216,33 +215,32 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement trackedRacesListComposite = new TrackedRacesListComposite(sailingService, errorReporter, regattaRefresher, stringMessages, /* multiselection */false, isActionButtonsEnabled()); + refreshableTrackedRaceSelectionModel = trackedRacesListComposite.getSelectionModel(); trackedRacesListComposite.ensureDebugId("TrackedRacesListComposite"); trackedRacesPanel.add(trackedRacesListComposite); trackedRacesListComposite.addTrackedRaceChangeListener(this); - this.raceSingleSelectionModel = (RefreshableSingleSelectionModel) trackedRacesListComposite.getSelectionModel(); - this.raceSingleSelectionModel.addSelectionChangeHandler(new Handler() { - @Override + trackedRaceListHandler = new SelectionChangeEvent.Handler() { + @Override public void onSelectionChange(SelectionChangeEvent event) { - if (reactOnSelectionChangeEvent) { - // if no leaderboard column is selected, ignore the race selection change - RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceColumnAndFleetName = getSelectedRaceColumnWithFleet(); - if (selectedRaceColumnAndFleetName != null) { - if (raceSingleSelectionModel.getSelectedSet().isEmpty()) { - if (selectedRaceColumnAndFleetName.getA() - .getRaceIdentifier(selectedRaceColumnAndFleetName.getB()) != null) { - unlinkRaceColumnFromTrackedRace( - selectedRaceColumnAndFleetName.getA().getRaceColumnName(), - selectedRaceColumnAndFleetName.getB()); - } - } else { - linkTrackedRaceToSelectedRaceColumn(selectedRaceColumnAndFleetName.getA(), - selectedRaceColumnAndFleetName.getB(), - raceSingleSelectionModel.getSelectedObject().getRaceIdentifier()); + Set selectedRaces = refreshableTrackedRaceSelectionModel.getSelectedSet(); + // if no leaderboard column is selected, ignore the race selection change + RaceColumnDTOAndFleetDTOWithNameBasedEquality selectedRaceColumnAndFleetName = getSelectedRaceColumnWithFleet(); + if (selectedRaceColumnAndFleetName != null) { + if (selectedRaces.isEmpty()) { + if (selectedRaceColumnAndFleetName.getA() + .getRaceIdentifier(selectedRaceColumnAndFleetName.getB()) != null) { + unlinkRaceColumnFromTrackedRace(selectedRaceColumnAndFleetName.getA().getRaceColumnName(), + selectedRaceColumnAndFleetName.getB()); } + } else { + linkTrackedRaceToSelectedRaceColumn(selectedRaceColumnAndFleetName.getA(), + selectedRaceColumnAndFleetName.getB(), + selectedRaces.iterator().next().getRaceIdentifier()); } } } - }); + }; + trackedRaceListHandlerRegistration = refreshableTrackedRaceSelectionModel.addSelectionChangeHandler(trackedRaceListHandler); Button reloadAllRaceLogs = new Button(stringMessages.reloadAllRaceLogs()); reloadAllRaceLogs.ensureDebugId("ReloadAllRaceLogsButton"); @@ -285,12 +283,9 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement } protected abstract void addLeaderboardControls(Panel controlsPanel); - protected abstract void addSelectedLeaderboardRacesControls(Panel racesPanel); - - protected abstract void addColumnsToLeaderboardTableAndSetSelectionModel( - FlushableCellTable leaderboardTable, AdminConsoleTableResources tableRes, - ListDataProvider leaderboardListDataProvider); + protected abstract void addColumnsToLeaderboardTableAndSetSelectionModel(FlushableCellTable leaderboardTable, + AdminConsoleTableResources tableRes, ListDataProvider listDataProvider); protected abstract void addColumnsToRacesTable(CellTable racesTable); protected SelectionCheckboxColumn createSortableSelectionCheckboxColumn( @@ -299,14 +294,12 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement SelectionCheckboxColumn selectionCheckboxColumn = new SelectionCheckboxColumn( tableResources.cellTableStyle().cellTableCheckboxSelected(), tableResources.cellTableStyle().cellTableCheckboxDeselected(), - tableResources.cellTableStyle().cellTableCheckboxColumnCell(), - new EntityIdentityComparator() { + tableResources.cellTableStyle().cellTableCheckboxColumnCell(), new EntityIdentityComparator() { @Override public boolean representSameEntity(StrippedLeaderboardDTO dto1, StrippedLeaderboardDTO dto2) { return dto1.name.equals(dto2.name); } - }, listDataProvider, leaderboardTable) { - }; + }, listDataProvider, leaderboardTable) {}; selectionCheckboxColumn.setSortable(true); leaderboardColumnListHandler.setComparator(selectionCheckboxColumn, new Comparator() { @Override @@ -322,6 +315,8 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement availableLeaderboardList.clear(); Util.addAll(leaderboards, availableLeaderboardList); filterLeaderboardPanel.updateAll(availableLeaderboardList); // also maintains the filtered leaderboardList + leaderboardSelectionChanged(); + leaderboardRaceColumnSelectionChanged(); } /** @@ -334,9 +329,15 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement new AsyncCallback() { @Override public void onSuccess(StrippedLeaderboardDTO leaderboard) { + for (StrippedLeaderboardDTO leaderboardDTO : leaderboardSelectionModel.getSelectedSet()) { + if (leaderboardDTO.name.equals(leaderboardName)) { + leaderboardSelectionModel.setSelected(leaderboardDTO, false); + break; + } + } replaceLeaderboardInList(availableLeaderboardList, leaderboardName, leaderboard); filterLeaderboardPanel.updateAll(availableLeaderboardList); // also updates leaderboardList provider - refreshableLeaderboardSelectionModel.setSelected(leaderboard, true); + leaderboardSelectionModel.setSelected(leaderboard, true); if (nameOfRaceColumnToSelect != null) { selectRaceColumn(nameOfRaceColumnToSelect); } @@ -351,17 +352,17 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement } })); } -//TODO If the list does not contains the element the last element will be replaced? + private void replaceLeaderboardInList(List leaderboardList, String leaderboardToReplace, StrippedLeaderboardDTO newLeaderboard) { int index = -1; for (StrippedLeaderboardDTO existingLeaderboard : leaderboardList) { index++; - if (existingLeaderboard.name.equals(leaderboardToReplace)) { // TODO Lukas: this should be the EntityIdentityComparator used for the RefreshableSelectionModel + if (existingLeaderboard.name.equals(leaderboardToReplace)) { break; } } if (index >= 0) { - leaderboardList.set(index, newLeaderboard); // TODO Lukas: update refreshable selection model accordingly + leaderboardList.set(index, newLeaderboard); } } @@ -532,7 +533,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement @Override public StrippedLeaderboardDTO getSelectedLeaderboard() { - return refreshableLeaderboardSelectionModel.getSelectedSet().isEmpty() ? null : refreshableLeaderboardSelectionModel.getSelectedSet().iterator().next(); + return leaderboardSelectionModel.getSelectedSet().isEmpty() ? null : leaderboardSelectionModel.getSelectedSet().iterator().next(); } protected LeaderboardsRefresher getLeaderboardsRefresher() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java index 939d847dfec..38b456d5ef8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java @@ -13,6 +13,8 @@ import java.util.Set; import com.google.gwt.cell.client.AbstractCell; import com.google.gwt.cell.client.FieldUpdater; 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.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.safehtml.client.SafeHtmlTemplates; @@ -118,7 +120,7 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { @Override public void onClick(ClickEvent event) { if (Window.confirm(stringMessages.doYouReallyWantToRemoveLeaderboards())) { - removeLeaderboards(refreshableLeaderboardSelectionModel.getSelectedSet()); + removeLeaderboards(leaderboardSelectionModel.getSelectedSet()); } } }); @@ -126,14 +128,12 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { } @Override - protected void addColumnsToLeaderboardTableAndSetSelectionModel( - final FlushableCellTable leaderboardTable, - AdminConsoleTableResources tableResources, - ListDataProvider leaderboardListDataProvider) { + protected void addColumnsToLeaderboardTableAndSetSelectionModel(final FlushableCellTable leaderboardTable, + AdminConsoleTableResources tableResources, ListDataProvider listDataProvider) { ListHandler leaderboardColumnListHandler = new ListHandler( leaderboardList.getList()); SelectionCheckboxColumn selectionCheckboxColumn = createSortableSelectionCheckboxColumn( - leaderboardTable, tableResources, leaderboardColumnListHandler, leaderboardListDataProvider); + leaderboardTable, tableResources, leaderboardColumnListHandler, listDataProvider); AnchorCell anchorCell = new AnchorCell(); Column linkColumn = new Column(anchorCell) { @Override @@ -675,13 +675,19 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { @Override protected void leaderboardSelectionChanged() { // make sure that clearing the selection doesn't cause an unlinking of the selected tracked race - reactOnSelectionChangeEvent = false; + trackedRaceListHandlerRegistration.removeHandler(); trackedRacesListComposite.clearSelection(); // add listener again using a scheduled command which is executed when the browser's event loop re-gains // control; we assume that at that point in time the selection updates have already been performed - leaderboardRemoveButton.setEnabled(!refreshableLeaderboardSelectionModel.getSelectedSet().isEmpty()); + Scheduler.get().scheduleFinally(new ScheduledCommand() { + @Override + public void execute() { + trackedRaceListHandlerRegistration = refreshableTrackedRaceSelectionModel.addSelectionChangeHandler(trackedRaceListHandler); + } + }); + leaderboardRemoveButton.setEnabled(!leaderboardSelectionModel.getSelectedSet().isEmpty()); StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard(); - if (refreshableLeaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { + if (leaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { raceColumnTable.getDataProvider().getList().clear(); for (RaceColumnDTO raceColumn : selectedLeaderboard.getRaceList()) { for (FleetDTO fleet : raceColumn.getFleets()) { @@ -701,7 +707,6 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { trackedRacesCaptionPanel.setVisible(false); selectedRaceInLeaderboard = null; } - reactOnSelectionChangeEvent = true; } private void createFlexibleLeaderboard() { @@ -782,8 +787,8 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { private void addLeaderboard(StrippedLeaderboardDTO result) { leaderboardList.getList().add(result); availableLeaderboardList.add(result); - refreshableLeaderboardSelectionModel.clear(); - refreshableLeaderboardSelectionModel.setSelected(result, true); + leaderboardSelectionModel.clear(); + leaderboardSelectionModel.setSelected(result, true); } private void updateLeaderboard(final String oldLeaderboardName, final LeaderboardDescriptor leaderboardToUpdate) { @@ -854,5 +859,6 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { private void removeLeaderboardFromTable(final StrippedLeaderboardDTO leaderBoard) { leaderboardList.getList().remove(leaderBoard); availableLeaderboardList.remove(leaderBoard); + leaderboardSelectionModel.setSelected(leaderBoard, false); } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java index d0df5db278e..84128ce0ca2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java @@ -86,13 +86,12 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC } @Override - protected void addColumnsToLeaderboardTableAndSetSelectionModel(FlushableCellTable leaderboardTable, - AdminConsoleTableResources tableResources, - ListDataProvider leaderboardListDataProvider) { + protected void addColumnsToLeaderboardTableAndSetSelectionModel(FlushableCellTable leaderboardTable, + AdminConsoleTableResources tableResources, ListDataProvider listDataProvider) { ListHandler leaderboardColumnListHandler = new ListHandler( leaderboardList.getList()); SelectionCheckboxColumn selectionCheckboxColumn = createSortableSelectionCheckboxColumn( - leaderboardTable, tableResources, leaderboardColumnListHandler, leaderboardListDataProvider); + leaderboardTable, tableResources, leaderboardColumnListHandler, listDataProvider); TextColumn leaderboardNameColumn = new TextColumn() { @Override public String getValue(StrippedLeaderboardDTO leaderboard) { @@ -377,7 +376,7 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC protected void leaderboardSelectionChanged() { StrippedLeaderboardDTO selectedLeaderboard = getSelectedLeaderboard(); regattaHasCompetitors = false; - if (refreshableLeaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { + if (leaderboardSelectionModel.getSelectedSet().size() == 1 && selectedLeaderboard != null) { raceColumnTable.getDataProvider().getList().clear(); for (RaceColumnDTO raceColumn : selectedLeaderboard.getRaceList()) { for (FleetDTO fleet : raceColumn.getFleets()) { @@ -390,7 +389,7 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC trackedRacesCaptionPanel.setVisible(true); } - sailingService.doesRegattaLogContainCompetitors(((StrippedLeaderboardDTO) refreshableLeaderboardSelectionModel.getSelectedSet().toArray()[0]).name, new RegattaLogCallBack()); + sailingService.doesRegattaLogContainCompetitors(((StrippedLeaderboardDTO) leaderboardSelectionModel.getSelectedSet().toArray()[0]).name, new RegattaLogCallBack()); } else { selectedLeaderBoardPanel.setVisible(false); trackedRacesCaptionPanel.setVisible(false); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java index a0f1bcaf40b..f54dbec81c5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/controls/SelectionCheckboxColumn.java @@ -43,6 +43,8 @@ import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel; * * @param */ + +// TODO Lukas: discuss with Axel change to non abstract class. (Here are no abstract methods anymore.) public abstract class SelectionCheckboxColumn extends AbstractSortableColumnWithMinMax { private final BetterCheckboxCell cell; private final String checkboxColumnCellCSSClass; From fc8e7cb6640412896195a1629acd532678a36b47 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 10 Dec 2015 11:29:16 +0100 Subject: [PATCH 145/306] added messaging hostname and exchange name to ReplicationMasterDTO and report it in ReplicationPanel Change-Id: Ifa6ffa11ad51627e57424087e1d55222da22a79c --- .../.settings/org.eclipse.core.resources.prefs | 2 ++ .../gwt/ui/adminconsole/ReplicationPanel.java | 3 ++- .../sap/sailing/gwt/ui/client/StringMessages.java | 2 +- .../gwt/ui/client/StringMessages.properties | 2 +- .../gwt/ui/client/StringMessages_de.properties | 2 +- .../gwt/ui/client/StringMessages_ru.properties | 2 +- .../gwt/ui/client/StringMessages_zh.properties | 2 +- .../sailing/gwt/ui/server/SailingServiceImpl.java | 4 ++-- .../gwt/ui/shared/ReplicationMasterDTO.java | 14 ++++++++++++-- .../.settings/org.eclipse.core.resources.prefs | 1 + 10 files changed, 24 insertions(+), 10 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs index 0f7d005f58f..c6979559171 100755 --- a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs +++ b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs @@ -4,8 +4,10 @@ encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_de.p encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ru.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_zh.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/server/HomeServiceUtil.java=utf-8 +encoding//src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel.java=utf-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/datamining/presentation/AbstractResultsPresenter.java=utf-8 +encoding//src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilterPanel.java=utf-8 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ReplicationPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ReplicationPanel.java index 5f0bd5fff7a..71707b47540 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ReplicationPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ReplicationPanel.java @@ -273,7 +273,8 @@ public class ReplicationPanel extends FlowPanel { errorReporter.reportPersistentInformation(stringMessages.warningServerIsReplica()); registeredMasters.insertRow(i); registeredMasters.setWidget(i, 0, new Label(stringMessages.replicatingFromMaster(replicatingFromMaster.getHostname(), - replicatingFromMaster.getMessagingPort(), replicatingFromMaster.getServletPort()))); + replicatingFromMaster.getMessagingPort(), replicatingFromMaster.getServletPort(), + replicatingFromMaster.getMessagingHostname(), replicatingFromMaster.getExchangeName()))); i++; addButton.setEnabled(false); stopReplicationButton.setEnabled(true); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 251c04e89b0..6cd222be5e9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -325,7 +325,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, c String messagingPortNumber(); String servletPortNumber(); String registeredAt(String string); - String replicatingFromMaster(String hostname, int jmsPort, int servletPort); + String replicatingFromMaster(String hostname, int jmsPort, int servletPort, String messagingHostname, String exchangeName); String showWindSpeedSeries(); String showWindDirectionSeries(); String fleet(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 3361c95bf25..e779c589504 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -336,7 +336,7 @@ courseMiddleLine=Course middle line messagingPortNumber=Messaging port number (0 for default) servletPortNumber=Servlet port number registeredAt=Registered at {0} -replicatingFromMaster=Replicating from master {0}, messaging port {1}, servlet port {2} +replicatingFromMaster=Replicating from master {0}:{2}, messaging port {3}:{1}, exchange {4} showWindSpeedSeries=Show wind speed data showWindDirectionSeries=Show wind directions data fleet=Fleet diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index 04cd149154d..18638a0e44a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -337,7 +337,7 @@ courseMiddleLine=Kursmittellinie messagingPortNumber=Messaging-Portnummer (0 für Default) servletPortNumber=Servlet Portnummer registeredAt=Registriert {0} -replicatingFromMaster=Repliziert von master {0}, Messaging Port {1}, Servlet Port {2} +replicatingFromMaster=Repliziert von master {0}:{2}, Messaging Port {3}:{1}, Exchange {4} showWindSpeedSeries=Daten für Windgeschwindigkeit zeigen showWindDirectionSeries=Daten für Windrichtung zeigen fleet=Flotte diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index 32f66fb642a..d714187072d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -329,7 +329,7 @@ courseMiddleLine=Средняя линия курса messagingPortNumber=Номер порта обмена сообщениями ( по умолчанию 0) servletPortNumber=Номер порта сервлета registeredAt=Зарегистрирован в {0} -replicatingFromMaster=Дублирование с основного {0}, порт передачи обмена сообщениями {1}, порт сервлета {2} +replicatingFromMaster=Replicating from master {0}:{2}, messaging port {3}:{1}, exchange {4} showWindSpeedSeries=Показать данные о силе ветра showWindDirectionSeries=Показать данные о направлении ветра fleet=Флот diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index 88bec57c775..093dcfa6756 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -328,7 +328,7 @@ setFixedPassing=Set time as mark passing maximumSpeedOverGroundInKnotsTooltip=比赛中参赛队的最大实际速度 twoMarkControlPoint=2-mark-controlpoint earlyStarters=Early starters -replicatingFromMaster=Replicating from master {0}, messaging port {1}, servlet port {2} +replicatingFromMaster=Replicating from master {0}:{2}, messaging port {3}:{1}, exchange {4} latitude=Latitude filterByLeaderboard=Filter by Leaderboard fromDeg=从 (角度) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 6fe3eb5aa90..cd60757787b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -3292,8 +3292,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S if (replicatingFromMaster == null) { master = null; } else { - master = new ReplicationMasterDTO(replicatingFromMaster.getHostname(), replicatingFromMaster.getMessagingPort(), - replicatingFromMaster.getServletPort()); + master = new ReplicationMasterDTO(replicatingFromMaster.getHostname(), replicatingFromMaster.getServletPort(), + replicatingFromMaster.getMessagingHostname(), replicatingFromMaster.getMessagingPort(), replicatingFromMaster.getExchangeName()); } return new ReplicationStateDTO(master, replicaDTOs, service.getServerIdentifier().toString()); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicationMasterDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicationMasterDTO.java index 76973fdec47..f9a4591133d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicationMasterDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/ReplicationMasterDTO.java @@ -6,12 +6,16 @@ public class ReplicationMasterDTO implements IsSerializable { private String hostname; private int messagingPort; private int servletPort; + private String messagingHostname; + private String exchangeName; ReplicationMasterDTO() {} - public ReplicationMasterDTO(String hostname, int messagingPort, int servletPort) { + public ReplicationMasterDTO(String hostname, int servletPort, String messagingHostname, int messagingPort, String exchangeName) { super(); this.hostname = hostname; - this.messagingPort = messagingPort; this.servletPort = servletPort; + this.messagingHostname = messagingHostname; + this.messagingPort = messagingPort; + this.exchangeName = exchangeName; } public String getHostname() { return hostname; @@ -22,4 +26,10 @@ public class ReplicationMasterDTO implements IsSerializable { public int getServletPort() { return servletPort; } + public String getMessagingHostname() { + return messagingHostname; + } + public String getExchangeName() { + return exchangeName; + } } \ No newline at end of file diff --git a/java/com.sap.sse.security.ui/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sse.security.ui/.settings/org.eclipse.core.resources.prefs index db751d38ff8..597b388e1ed 100755 --- a/java/com.sap.sse.security.ui/.settings/org.eclipse.core.resources.prefs +++ b/java/com.sap.sse.security.ui/.settings/org.eclipse.core.resources.prefs @@ -1,2 +1,3 @@ eclipse.preferences.version=1 encoding//src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_de.properties=UTF-8 +encoding//src/main/java/com/sap/sse/security/ui/servlet/RedirectServlet.java=utf-8 From 9bb8a580c0916c7059751611e9aeb24954c46843 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 10 Dec 2015 13:54:35 +0100 Subject: [PATCH 146/306] added mark position finder for convertToRaceCourseDTO invocation to avoid NPE; see also bug 3406 Change-Id: I738319ef9daef3342cdf71ba665154325f26da36 --- .../sailing/gwt/ui/server/SailingServiceImpl.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index cd60757787b..11cd2e1b1c5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -864,8 +864,9 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S private RaceInfoDTO createRaceInfoDTO(String seriesName, RaceColumn raceColumn, Fleet fleet) { RaceInfoDTO raceInfoDTO = new RaceInfoDTO(); RaceLog raceLog = raceColumn.getRaceLog(fleet); + final TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); + raceInfoDTO.isTracked = trackedRace != null ? true : false; if (raceLog != null) { - raceInfoDTO.isTracked = raceColumn.getTrackedRace(fleet) != null ? true : false; ReadonlyRaceState state = ReadonlyRaceStateImpl.create(getService(), raceLog); TimePoint startTime = state.getStartTime(); if (startTime != null) { @@ -881,13 +882,14 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } else { raceInfoDTO.finishedTime = null; if (raceInfoDTO.isTracked) { - TimePoint endOfRace = raceColumn.getTrackedRace(fleet).getEndOfRace(); + TimePoint endOfRace = trackedRace.getEndOfRace(); raceInfoDTO.finishedTime = endOfRace != null ? endOfRace.asDate() : null; } } + final TimePoint now = MillisecondsTimePoint.now(); if (startTime != null) { - FlagPoleState activeFlagState = state.getRacingProcedure().getActiveFlags(startTime, MillisecondsTimePoint.now()); + FlagPoleState activeFlagState = state.getRacingProcedure().getActiveFlags(startTime, now); List activeFlags = activeFlagState.getCurrentState(); FlagPoleState previousFlagState = activeFlagState.getPreviousState(state.getRacingProcedure(), startTime); List previousFlags = previousFlagState.getCurrentState(); @@ -919,7 +921,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S CourseBase lastCourse = state.getCourseDesign(); if (lastCourse != null) { - raceInfoDTO.lastCourseDesign = convertToRaceCourseDTO(lastCourse, /* trackedRace */ null, /* timePoint */ null); + raceInfoDTO.lastCourseDesign = convertToRaceCourseDTO(lastCourse, new TrackedRaceMarkPositionFinder(trackedRace), now); raceInfoDTO.lastCourseName = lastCourse.getName(); } @@ -937,7 +939,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S Wind wind = state.getWindFix(); if (wind != null) { - raceInfoDTO.lastWind = createWindDTOFromAlreadyAveraged(wind, MillisecondsTimePoint.now()); + raceInfoDTO.lastWind = createWindDTOFromAlreadyAveraged(wind, now); } fillStartProcedureSpecifics(raceInfoDTO, state); @@ -947,7 +949,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S raceInfoDTO.fleetName = fleet.getName(); raceInfoDTO.fleetOrdering = fleet.getOrdering(); raceInfoDTO.raceIdentifier = raceColumn.getRaceIdentifier(fleet); - raceInfoDTO.isTracked = raceColumn.getTrackedRace(fleet) != null ? true : false; return raceInfoDTO; } From 4122cfc81bc414c2682db27bcce7c22220bfe03f Mon Sep 17 00:00:00 2001 From: fmittag Date: Fri, 11 Dec 2015 03:26:51 +1100 Subject: [PATCH 147/306] added a information field 'timeZone' to the EventDialog showing the locale browser timezone as an indicator which time you are entering when you set the start and end date of an event --- .../gwt/ui/adminconsole/EventDialog.java | 26 ++++++++++--------- .../sailing/gwt/ui/client/StringMessages.java | 1 + .../gwt/ui/client/StringMessages.properties | 1 + .../ui/client/StringMessages_de.properties | 1 + .../ui/client/StringMessages_ru.properties | 1 + .../ui/client/StringMessages_zh.properties | 1 + .../client/DateAndTimeFormatterUtil.java | 8 ++++++ 7 files changed, 27 insertions(+), 12 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java index 51bd281f3dd..5c548c58620 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventDialog.java @@ -24,6 +24,7 @@ import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.ui.client.DataEntryDialogWithBootstrap; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; import com.sap.sailing.gwt.ui.shared.BetterDateTimeBox; import com.sap.sailing.gwt.ui.shared.CourseAreaDTO; import com.sap.sailing.gwt.ui.shared.EventDTO; @@ -195,24 +196,25 @@ public abstract class EventDialog extends DataEntryDialogWithBootstrap if (additionalWidget != null) { panel.add(additionalWidget); } - - Grid formGrid = new Grid(8, 2); + Grid formGrid = new Grid(9, 2); formGrid.setWidget(0, 0, new Label(stringMessages.name() + ":")); formGrid.setWidget(0, 1, nameEntryField); formGrid.setWidget(1, 0, new Label(stringMessages.description() + ":")); formGrid.setWidget(1, 1, descriptionEntryField); formGrid.setWidget(2, 0, new Label(stringMessages.venue() + ":")); formGrid.setWidget(2, 1, venueEntryField); - formGrid.setWidget(3, 0, new Label(stringMessages.startDate() + ":")); - formGrid.setWidget(3, 1, startDateBox); - formGrid.setWidget(4, 0, new Label(stringMessages.endDate() + ":")); - formGrid.setWidget(4, 1, endDateBox); - formGrid.setWidget(5, 0, new Label(stringMessages.isPublic() + ":")); - formGrid.setWidget(5, 1, isPublicCheckBox); - formGrid.setWidget(6, 0, new Label(stringMessages.eventOfficialWebsiteURL() + ":")); - formGrid.setWidget(6, 1, officialWebsiteURLEntryField); - formGrid.setWidget(7, 0, new Label(stringMessages.eventSailorsInfoWebsiteURL() + ":")); - formGrid.setWidget(7, 1, sailorsInfoWebsiteURLEntryField); + formGrid.setWidget(3, 0, new Label(stringMessages.timeZone() + ":")); + formGrid.setWidget(3, 1, new Label(DateAndTimeFormatterUtil.getClientTimeZoneAsGMTString())); + formGrid.setWidget(4, 0, new Label(stringMessages.startDate() + ":")); + formGrid.setWidget(4, 1, startDateBox); + formGrid.setWidget(5, 0, new Label(stringMessages.endDate() + ":")); + formGrid.setWidget(5, 1, endDateBox); + formGrid.setWidget(6, 0, new Label(stringMessages.isPublic() + ":")); + formGrid.setWidget(6, 1, isPublicCheckBox); + formGrid.setWidget(7, 0, new Label(stringMessages.eventOfficialWebsiteURL() + ":")); + formGrid.setWidget(7, 1, officialWebsiteURLEntryField); + formGrid.setWidget(8, 0, new Label(stringMessages.eventSailorsInfoWebsiteURL() + ":")); + formGrid.setWidget(8, 1, sailorsInfoWebsiteURLEntryField); TabLayoutPanel tabPanel = new TabLayoutPanel(30, Unit.PX); tabPanel.setHeight("500px"); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index 6cd222be5e9..100aa7e5a99 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -1111,6 +1111,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, c String invalidSelection(); String advancePassId(); String pleaseSelectAScoringResult(); + String timeZone(); String eventOfficialWebsiteURL(); String eventSailorsInfoWebsiteURL(); String eventOverviewURL(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index e779c589504..957de470771 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -1082,6 +1082,7 @@ sponsorImageURLs=Sponsor Image URLs invalidSelection=The current selection isn''t valid. advancePassId=Advance Pass ID pleaseSelectAScoringResult=Please select a scoring result... +timeZone=Time zone eventOfficialWebsiteURL=Official Website URL eventSailorsInfoWebsiteURL=Sailors Info Website URL eventOverviewURL=Event Overview URL diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index 18638a0e44a..86208c32137 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -1077,6 +1077,7 @@ sponsorImageURLs=Sponsorenlogo-URLs invalidSelection=Die aktuelle Selektion ist nicht gültig. advancePassId=Pass ID erhöhen pleaseSelectAScoringResult=Bitte ein Ergebnis auswählen... +timeZone=Zeitzone eventOfficialWebsiteURL=URL der offiziellen Veranstaltungswebseite eventSailorsInfoWebsiteURL=URL zur Webseite mit Seglerinfos eventOverviewURL=URL der Veranstaltungsübersicht diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index d714187072d..a29a04b8fe7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -1030,6 +1030,7 @@ imageURLs=Image URLs videoURLs=Video URLs sponsorImageURLs=Sponsor Image URLs pleaseSelectAScoringResult=Please select a scoring result... +timeZone=Time zone eventOfficialWebsiteURL=Official Website URL eventSailorsInfoWebsiteURL=Sailors Info Website URL eventOverviewURL=Event Overview URL diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index 093dcfa6756..dbd13fad0d2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -840,6 +840,7 @@ refreshIntervalMustBeGreaterThanXSeconds=刷新间隔必须大于0秒 {0} authorName=Author name raceIsInStartphase=开始阶段 pleaseSelectAScoringResult=Please select a scoring result... +timeZone=Time zone portSide=左舷 explainReplicationHostname=Hostname where the replication master is running on. igtimiAccounts=Igtimi Accounts diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/common/client/DateAndTimeFormatterUtil.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/common/client/DateAndTimeFormatterUtil.java index 3938fc8090b..44b43feb9e6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/common/client/DateAndTimeFormatterUtil.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/common/client/DateAndTimeFormatterUtil.java @@ -60,4 +60,12 @@ public class DateAndTimeFormatterUtil { return result; } + + public static String getClientTimeZoneAsGMTString() { + Date now = new Date(); + @SuppressWarnings("deprecation") + int localeTimezoneOffset = now.getTimezoneOffset(); + TimeZone localeTimeZone = TimeZone.createTimeZone(localeTimezoneOffset); + return localeTimeZone.getGMTString(now); + } } From ba190c7ad83adf0f482dd9d73582c7c2c192be84 Mon Sep 17 00:00:00 2001 From: fmittag Date: Thu, 10 Dec 2015 17:34:17 +0100 Subject: [PATCH 148/306] added the timezone hint to the regatta dialog as well --- ...tractRegattaWithSeriesAndFleetsDialog.java | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRegattaWithSeriesAndFleetsDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRegattaWithSeriesAndFleetsDialog.java index b18109f4e2f..07a068228fc 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRegattaWithSeriesAndFleetsDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractRegattaWithSeriesAndFleetsDialog.java @@ -21,6 +21,7 @@ import com.sap.sailing.domain.common.RankingMetrics; import com.sap.sailing.domain.common.ScoringSchemeType; import com.sap.sailing.gwt.ui.client.DataEntryDialogWithBootstrap; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.common.client.DateAndTimeFormatterUtil; import com.sap.sailing.gwt.ui.leaderboard.RankingMetricTypeFormatter; import com.sap.sailing.gwt.ui.leaderboard.ScoringSchemeTypeFormatter; import com.sap.sailing.gwt.ui.shared.BetterDateTimeBox; @@ -134,20 +135,23 @@ public abstract class AbstractRegattaWithSeriesAndFleetsDialog extends DataEn if (additionalWidget != null) { panel.add(additionalWidget); } - Grid formGrid = new Grid(6, 2); + Grid formGrid = new Grid(7, 2); panel.add(formGrid); - formGrid.setWidget(0, 0, new Label(stringMessages.startDate() + ":")); - formGrid.setWidget(0, 1, startDateBox); - formGrid.setWidget(1, 0, new Label(stringMessages.endDate() + ":")); - formGrid.setWidget(1, 1, endDateBox); - formGrid.setWidget(2, 0, new Label(stringMessages.scoringSystem() + ":")); - formGrid.setWidget(2, 1, scoringSchemeListBox); - formGrid.setWidget(3, 0, new Label(stringMessages.event() + ":")); - formGrid.setWidget(3, 1, sailingEventsListBox); - formGrid.setWidget(4, 0, new Label(stringMessages.courseArea() + ":")); - formGrid.setWidget(4, 1, courseAreaListBox); - formGrid.setWidget(5, 0, new Label(stringMessages.useStartTimeInference() + ":")); - formGrid.setWidget(5, 1, useStartTimeInferenceCheckBox); + + formGrid.setWidget(0, 0, new Label(stringMessages.timeZone() + ":")); + formGrid.setWidget(0, 1, new Label(DateAndTimeFormatterUtil.getClientTimeZoneAsGMTString())); + formGrid.setWidget(1, 0, new Label(stringMessages.startDate() + ":")); + formGrid.setWidget(1, 1, startDateBox); + formGrid.setWidget(2, 0, new Label(stringMessages.endDate() + ":")); + formGrid.setWidget(2, 1, endDateBox); + formGrid.setWidget(3, 0, new Label(stringMessages.scoringSystem() + ":")); + formGrid.setWidget(3, 1, scoringSchemeListBox); + formGrid.setWidget(4, 0, new Label(stringMessages.event() + ":")); + formGrid.setWidget(4, 1, sailingEventsListBox); + formGrid.setWidget(5, 0, new Label(stringMessages.courseArea() + ":")); + formGrid.setWidget(5, 1, courseAreaListBox); + formGrid.setWidget(6, 0, new Label(stringMessages.useStartTimeInference() + ":")); + formGrid.setWidget(6, 1, useStartTimeInferenceCheckBox); setupAdditionalWidgetsOnPanel(panel, formGrid); return panel; } From a35112bcaf90fc111196b96a1ca27d8b63c08d62 Mon Sep 17 00:00:00 2001 From: fmittag Date: Thu, 10 Dec 2015 19:07:30 +0100 Subject: [PATCH 149/306] As a fast workaround for bug 3366 I changed the logic that also live races of 'upcoming' events are considered for the live races list --- .../event/GetRegattasAndLiveRacesForEventAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetRegattasAndLiveRacesForEventAction.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetRegattasAndLiveRacesForEventAction.java index e6c9cf92821..b70f6c18a2c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetRegattasAndLiveRacesForEventAction.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/communication/event/GetRegattasAndLiveRacesForEventAction.java @@ -43,7 +43,7 @@ public class GetRegattasAndLiveRacesForEventAction implements SailingAction Date: Thu, 10 Dec 2015 19:19:44 +0100 Subject: [PATCH 150/306] newSelectionModel: some preparation for new UI Test for SmartphoneEventManagementTrackingPanel --- .../AbstractLeaderboardConfigPanel.java | 1 + .../adminconsole/AdminConsoleEntryPoint.java | 1 + .../gwt/ui/adminconsole/TableWrapper.java | 1 + .../pages/adminconsole/AdminConsolePage.java | 9 +++++ ...rtphoneTrackingEventManagementPanelPO.java | 40 +++++++++++++++++++ .../tracking/RaceColumnTableWrapperPO.java | 24 +++++++++++ .../tracking/TrackedRacesListPO.java | 2 +- 7 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/connectors/SmartphoneTrackingEventManagementPanelPO.java create mode 100644 java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/tracking/RaceColumnTableWrapperPO.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index 2d0da3dabde..b28db15f5f9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -261,6 +261,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement vPanel.add(lblRaceNamesIn); raceColumnTable = new RaceTableWrapper>( sailingService, stringMessages, errorReporter, multiSelection); + raceColumnTable.asWidget().ensureDebugId("RaceColumnTable"); raceColumnTable.getTable().setWidth("100%"); addColumnsToRacesTable(raceColumnTable.getTable()); this.raceColumnTableSelectionModel = raceColumnTable.getSelectionModel(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java index 6f1aa6e53ae..fed88bb39f8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java @@ -181,6 +181,7 @@ public class AdminConsoleEntryPoint extends AbstractSailingEntryPoint implements final SmartphoneTrackingEventManagementPanel raceLogTrackingEventManagementPanel = new SmartphoneTrackingEventManagementPanel( sailingService, this, this, this, getStringMessages()); + raceLogTrackingEventManagementPanel.ensureDebugId("SmartphoneTrackingPanel"); panel.addToTabPanel(connectorsTabPanel, new DefaultRefreshableAdminConsolePanel(raceLogTrackingEventManagementPanel) { @Override public void refreshAfterBecomingVisible() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java index 0c4d41fa54e..ead40d8f7dd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java @@ -46,6 +46,7 @@ public abstract class TableWrapper> im this.sailingService = sailingService; this.errorReporter = errorReporter; table = new FlushableCellTable(10000, tableRes); + table.ensureDebugId("WrappedTable"); this.dataProvider = new ListDataProvider(); this.columnSortHandler = new ListHandler(dataProvider.getList()); table.addColumnSortHandler(this.columnSortHandler); diff --git a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/AdminConsolePage.java b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/AdminConsolePage.java index 4b56dafba9c..507c762dd5b 100755 --- a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/AdminConsolePage.java +++ b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/AdminConsolePage.java @@ -15,6 +15,7 @@ import com.sap.sailing.selenium.core.BySeleniumId; import com.sap.sailing.selenium.core.ElementSearchConditions; import com.sap.sailing.selenium.core.FindBy; import com.sap.sailing.selenium.pages.HostPage; +import com.sap.sailing.selenium.pages.adminconsole.connectors.SmartphoneTrackingEventManagementPanelPO; import com.sap.sailing.selenium.pages.adminconsole.leaderboard.LeaderboardConfigurationPanelPO; import com.sap.sailing.selenium.pages.adminconsole.leaderboard.LeaderboardGroupConfigurationPanelPO; import com.sap.sailing.selenium.pages.adminconsole.regatta.RegattaStructureManagementPanelPO; @@ -66,6 +67,8 @@ public class AdminConsolePage extends HostPage { private static final String COMPETITOR_PANEL_TAB_LABEL = "Competitors"; //$NON-NLS-1$ private static final String COMPETITOR_PANEL_TAB_IDENTIFIER = "CompetitorPanel"; //$NON-NLS-1$ + private static final String SMARTPHONETRACKINGPANEL_PANEL_TAB_LABEL = "Smartphone Tracking"; //$NON-NLS-1$ + private static final String SMARTPHONETRACKINGPANEL_PANEL_TAB_IDENTIFIER = "SmartphoneTrackingPanel"; //$NON-NLS-1$ /** *

    Goes to the administration console and returns the representing page object.

    * @@ -130,6 +133,12 @@ public class AdminConsolePage extends HostPage { COMPETITOR_PANEL_TAB_IDENTIFIER, false)); } + public SmartphoneTrackingEventManagementPanelPO goToSmartphoneTrackingPanel() { + goToTab(TRACTRAC_EVENTS_TAB_PARENT_LABEL, TRACTRAC_EVENTS_TAB_PARENT_IDENTIFIER, true); + return new SmartphoneTrackingEventManagementPanelPO(this.driver, goToTab(SMARTPHONETRACKINGPANEL_PANEL_TAB_LABEL, + SMARTPHONETRACKINGPANEL_PANEL_TAB_IDENTIFIER, false)); + } + /** *

    Verifies that the current page is the administration console by checking the title of the page.

    */ diff --git a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/connectors/SmartphoneTrackingEventManagementPanelPO.java b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/connectors/SmartphoneTrackingEventManagementPanelPO.java new file mode 100644 index 00000000000..f49f719c273 --- /dev/null +++ b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/connectors/SmartphoneTrackingEventManagementPanelPO.java @@ -0,0 +1,40 @@ +package com.sap.sailing.selenium.pages.adminconsole.connectors; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import com.sap.sailing.selenium.core.BySeleniumId; +import com.sap.sailing.selenium.core.FindBy; +import com.sap.sailing.selenium.pages.PageArea; +import com.sap.sailing.selenium.pages.adminconsole.tracking.RaceColumnTableWrapperPO; +import com.sap.sailing.selenium.pages.adminconsole.tracking.TrackedRacesListPO; +import com.sap.sailing.selenium.pages.gwt.CellTablePO; +import com.sap.sailing.selenium.pages.gwt.DataEntryPO; +import com.sap.sailing.selenium.pages.gwt.GenericCellTablePO; + +public class SmartphoneTrackingEventManagementPanelPO extends PageArea { + @FindBy(how = BySeleniumId.class, using = "AvailableLeaderboardsTable") + private WebElement leaderboardTable; + + @FindBy(how = BySeleniumId.class, using = "RaceColumnTable") + private WebElement raceColumnTableWrapper; + + @FindBy(how = BySeleniumId.class, using = "TrackedRacesListComposite") + private WebElement trackedRacesListComposite; + + public SmartphoneTrackingEventManagementPanelPO(WebDriver driver, WebElement element) { + super(driver, element); + } + + public CellTablePO getLeaderboardTable() { + return new GenericCellTablePO<>(this.driver, this.leaderboardTable, DataEntryPO.class); + } + + public RaceColumnTableWrapperPO getRaceColumnTableWrapper() { + return new RaceColumnTableWrapperPO(this.driver, this.raceColumnTableWrapper); + } + + public TrackedRacesListPO getTrackedRaceListComposite() { + return new TrackedRacesListPO(this.driver, this.trackedRacesListComposite); + } +} diff --git a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/tracking/RaceColumnTableWrapperPO.java b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/tracking/RaceColumnTableWrapperPO.java new file mode 100644 index 00000000000..03b72d75a03 --- /dev/null +++ b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/tracking/RaceColumnTableWrapperPO.java @@ -0,0 +1,24 @@ +package com.sap.sailing.selenium.pages.adminconsole.tracking; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import com.sap.sailing.selenium.core.BySeleniumId; +import com.sap.sailing.selenium.core.FindBy; +import com.sap.sailing.selenium.pages.PageArea; +import com.sap.sailing.selenium.pages.gwt.CellTablePO; +import com.sap.sailing.selenium.pages.gwt.DataEntryPO; +import com.sap.sailing.selenium.pages.gwt.GenericCellTablePO; + +public class RaceColumnTableWrapperPO extends PageArea { + @FindBy(how = BySeleniumId.class, using = "WrappedTable") + private WebElement raceColumnTable; + + public RaceColumnTableWrapperPO(WebDriver driver, WebElement element) { + super(driver,element); + } + + public CellTablePO getRaceColumnTable() { + return new GenericCellTablePO<>(this.driver, raceColumnTable, DataEntryPO.class); + } +} diff --git a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/tracking/TrackedRacesListPO.java b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/tracking/TrackedRacesListPO.java index 810a9c075b7..2f1252d119f 100644 --- a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/tracking/TrackedRacesListPO.java +++ b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/pages/adminconsole/tracking/TrackedRacesListPO.java @@ -257,7 +257,7 @@ public class TrackedRacesListPO extends PageArea { } } - private CellTablePO getTrackedRacesTable() { + public CellTablePO getTrackedRacesTable() { // if(!this.trackedRacesTable.isDisplayed()) { // return null; // } From e50e8480cf5934afa8a22d67af08e5b5d3b7044d Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 11 Dec 2015 08:08:34 +0100 Subject: [PATCH 151/306] fix for bug 3409: don't do a blocking wait for TrackedRace while responding to a REST request but return null instead Change-Id: I470e68a557300ab14e82a7e7749fb9e9430fa381 --- .../server/gateway/jaxrs/AbstractSailingServerResource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/AbstractSailingServerResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/AbstractSailingServerResource.java index 419781c0888..3571d969bfa 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/AbstractSailingServerResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/AbstractSailingServerResource.java @@ -76,8 +76,8 @@ public abstract class AbstractSailingServerResource { TrackedRace trackedRace = null; if (regatta != null && race != null) { DynamicTrackedRegatta trackedRegatta = getService().getTrackedRegatta(regatta); - if(trackedRegatta != null) { - trackedRace = trackedRegatta.getTrackedRace(race); + if (trackedRegatta != null) { + trackedRace = trackedRegatta.getExistingTrackedRace(race); } } return trackedRace; From f739178b8d277256465dd29bf7135e7b8fd40a68 Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Fri, 11 Dec 2015 10:20:32 +0100 Subject: [PATCH 152/306] improved WindBotComponent and VerticalWindChart performance after Chrome CPU profiling --- .../client/windchart/VerticalWindChart.java | 89 +++++++----- .../client/windchart/WindBotComponent.java | 135 ++++++++---------- 2 files changed, 111 insertions(+), 113 deletions(-) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java index 9a2613e0481..5d861763a14 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/VerticalWindChart.java @@ -3,7 +3,6 @@ package com.sap.sailing.dashboards.gwt.client.windchart; import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.logging.Level; import java.util.logging.Logger; import org.moxieapps.gwt.highcharts.client.Axis.Type; @@ -39,6 +38,9 @@ import com.google.gwt.user.client.ui.FocusPanel; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.dashboards.gwt.client.util.HighchartsUtil; +import com.sap.sailing.dashboards.gwt.shared.WindType; +import com.sap.sailing.gwt.ui.client.shared.charts.ChartPointRecalculator; +import com.sap.sailing.gwt.ui.shared.WindDTO; /** * The class represents a wind chart that is displayed vertically. Because it is meant to be used to display wind fixes, @@ -52,8 +54,7 @@ import com.sap.sailing.dashboards.gwt.client.util.HighchartsUtil; public class VerticalWindChart extends Composite implements HasWidgets { private Series verticalWindChartSeries; - private String positiveSeriesColorAsHex; - private String negativeSeriesColorAsHex; + private Point lastPoint; private List verticalWindChartClickListeners; /** @@ -73,6 +74,7 @@ public class VerticalWindChart extends Composite implements HasWidgets { private static VerticalWindChartUiBinder uiBinder = GWT.create(VerticalWindChartUiBinder.class); + @SuppressWarnings("unused") private static final Logger logger = Logger.getLogger(VerticalWindChart.class.getName()); interface VerticalWindChartUiBinder extends UiBinder { @@ -110,8 +112,7 @@ public class VerticalWindChart extends Composite implements HasWidgets { setChartOptions(); setXAxisOptions(); setYAxisOptions(); - positiveSeriesColorAsHex = positiveFillColor; - negativeSeriesColorAsHex = negativeFillColor; + initVerticalWindChartSeries(positiveFillColor, negativeFillColor); verticalWindChartClickListeners = new ArrayList(); HighchartsUtil.setSizeToMatchContainerDelayed(verticalWindChart); } @@ -171,13 +172,13 @@ public class VerticalWindChart extends Composite implements HasWidgets { })); } - private void initVerticalWindChartSeries() { + private void initVerticalWindChartSeries(String positivSeriesColorAsHex, String negativeSeriesColorAsHex) { verticalWindChartSeries = verticalWindChart.createSeries(); AreaPlotOptions areaPlotOptions = new AreaPlotOptions(); verticalWindChart.addSeries(verticalWindChartSeries.setName(null).setPlotOptions( areaPlotOptions.setDashStyle(DashStyle.SOLID).setLineWidth(0.1) .setMarker(new Marker().setEnabled(false)).setShadow(false).setHoverStateEnabled(false) - .setLineColor("#FFFFFF").setFillColor(positiveSeriesColorAsHex) + .setLineColor("#FFFFFF").setFillColor(positivSeriesColorAsHex) .setOption("negativeFillColor", negativeSeriesColorAsHex)).setOption("turboThreshold", MAX_SERIES_POINTS)); } @@ -190,27 +191,52 @@ public class VerticalWindChart extends Composite implements HasWidgets { * {@link #setXAxisExtremesForSeriesPointRangeIsSmallerThanChartDisplayIntervall()} and * {@link #setXAxisExtremesForSeriesPointRangeIsBiggerThanChartDisplayIntervall()}. * */ - public void addPointsToSeriesWithAverage(final Point[] points, final double average) { - if (verticalWindChartSeries == null) { - initVerticalWindChartSeries(); - verticalWindChartSeries.setPoints(points, true); - logger.log(Level.INFO, "Set Points"); + public void addPointsToSeriesWithAverageAndWindType(List windFixes, final double average, WindType windType) { + if(windType.equals(WindType.SPEED)) { + addPointsToSeriesOfTypeSpeed(windFixes); } else { - for (Point point : points) { - verticalWindChartSeries.addPoint(point, true, false, false); - logger.log(Level.INFO, "Add Point"); - } + addPointsToSeriesOfTypeDirection(windFixes); + } + verticalWindChartSeries.updateThreshold(""+average); + adaptVerticalWindChartExtemes(lastPoint.getX().longValue()); + } + + private Point createSpeedPoint(WindDTO windDTO) { + Point result = new Point(windDTO.measureTimepoint, windDTO.trueWindSpeedInKnots); + if (lastPoint != null) { + adaptWindDirectionPointToStayCloseToLastPoint(lastPoint, result); + } + lastPoint = result; + return result; + } + + private Point createDirectionPoint(WindDTO windDTO) { + Point result = new Point(windDTO.measureTimepoint, windDTO.trueWindFromDeg); + if (lastPoint != null) { + adaptWindDirectionPointToStayCloseToLastPoint(lastPoint, result); + } + lastPoint = result; + return result; + } + + private void addPointsToSeriesOfTypeSpeed(List windFixes) { + for (WindDTO windDTO : windFixes) { + verticalWindChartSeries.addPoint(createSpeedPoint(windDTO), false, false, false); + } + } + + private void addPointsToSeriesOfTypeDirection(List windFixes) { + for (WindDTO windDTO : windFixes) { + verticalWindChartSeries.addPoint(createDirectionPoint(windDTO), false, false, false); } - verticalWindChartSeries.updateThreshold("" + average); - adaptVerticalWindChartExtemes(); } /** * Depending on the series points time range and the selected display interval, the series fits into the display * interval or not. So there are two cases where the extreme points of the x-axis get calculated differently. * */ - private void adaptVerticalWindChartExtemes() { - setXAxisExtremesForSeriesPointRangeIsBiggerThanChartDisplayIntervall(); + private void adaptVerticalWindChartExtemes(Long lastTimePoint) { + setXAxisExtremesForSeriesPointRangeIsBiggerThanChartDisplayIntervall(lastTimePoint.longValue()); } /** @@ -219,25 +245,12 @@ public class VerticalWindChart extends Composite implements HasWidgets { * the the max value minus the selected display interval and the max value the last points time value. The chart * gets other extremes, because the values do not fit anymore into the selected display interval. * */ - private void setXAxisExtremesForSeriesPointRangeIsBiggerThanChartDisplayIntervall() { - long maximumExtremeValueInMillis = getLastPointOfVerticalWindChartSeries().getX().longValue(); + private void setXAxisExtremesForSeriesPointRangeIsBiggerThanChartDisplayIntervall(long lastTimePoint) { + long maximumExtremeValueInMillis = lastTimePoint; long minimumExtremeValueInMillis = maximumExtremeValueInMillis - chartIntervallinMinutes * 60 * 1000; verticalWindChart.getXAxis().setExtremes(minimumExtremeValueInMillis, maximumExtremeValueInMillis, true, false); } - - private Point getLastPointOfVerticalWindChartSeries() { - Point lastPointOfVerticalWindChartSeries = null; - if (verticalWindChartSeries != null) { - Point[] seriesPoints = verticalWindChartSeries.getPoints(); - int seriesPointsLenght = seriesPoints.length; - if (seriesPointsLenght > 0) { - lastPointOfVerticalWindChartSeries = seriesPoints[seriesPointsLenght - 1]; - } - } - return lastPointOfVerticalWindChartSeries; - } - public void addVerticalWindChartClickListener(VerticalWindChartClickListener verticalWindChartClickListener) { verticalWindChartClickListeners.add(verticalWindChartClickListener); } @@ -251,6 +264,10 @@ public class VerticalWindChart extends Composite implements HasWidgets { verticalWindChartClickListener.clickedWindChartWithNewIntervalChangeInMillis(intervalInMillis); } } + + private Point adaptWindDirectionPointToStayCloseToLastPoint(Point previousPoint, Point point) { + return ChartPointRecalculator.stayClosestToPreviousPoint(previousPoint, point); + } /** * The method handles a click on the vertical wind charts focus panel. It changes the display interval of the x-axis @@ -263,7 +280,7 @@ public class VerticalWindChart extends Composite implements HasWidgets { } else { chartIntervallinMinutes = SMALL_DISPLAY_INTERVALL_IN_MINUTES; } - adaptVerticalWindChartExtemes(); + adaptVerticalWindChartExtemes(lastPoint.getX().longValue()); notifyVerticalWindChartClickListeners(chartIntervallinMinutes); } diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/WindBotComponent.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/WindBotComponent.java index 04b3062b75e..215429c2015 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/WindBotComponent.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/windchart/WindBotComponent.java @@ -5,14 +5,12 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; -import org.moxieapps.gwt.highcharts.client.Point; - import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.resources.client.CssResource; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; -import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.HasWidgets; @@ -21,12 +19,11 @@ import com.sap.sailing.dashboards.gwt.client.dataretriever.WindBotDataRetrieverL import com.sap.sailing.dashboards.gwt.client.startlineadvantage.LiveAverageComponent; import com.sap.sailing.dashboards.gwt.client.windchart.compass.LocationPointerCompass; import com.sap.sailing.dashboards.gwt.shared.MovingAverage; -import com.sap.sailing.dashboards.gwt.shared.SinWave; import com.sap.sailing.dashboards.gwt.shared.WindType; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.gwt.dispatch.client.batching.SplitScheduler; import com.sap.sailing.gwt.ui.client.StringMessages; -import com.sap.sailing.gwt.ui.client.shared.charts.ChartPointRecalculator; import com.sap.sailing.gwt.ui.shared.WindDTO; import com.sap.sailing.gwt.ui.shared.WindInfoForRaceDTO; import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; @@ -43,15 +40,10 @@ import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; */ public class WindBotComponent extends Composite implements HasWidgets, WindBotDataRetrieverListener { - private SinWave speedSinWave; - private SinWave directionSinWave; - private boolean inSimulationMode; - private MovingAverage movingAverageSpeed; private MovingAverage movingAverageDirection; private static WindBotComponentUiBinder uiBinder = GWT.create(WindBotComponentUiBinder.class); - private static final String IN_SIMULATION_MODE = "simulation"; private static final Logger logger = Logger.getLogger(WindBotComponent.class.getName()); interface WindBotComponentUiBinder extends UiBinder { @@ -111,14 +103,6 @@ public class WindBotComponent extends Composite implements HasWidgets, WindBotDa public WindBotComponent(String windBotId) { this.windBotId = windBotId; stringConstants = StringMessages.INSTANCE; - String inSimulationModeParameter = Window.Location.getParameter(IN_SIMULATION_MODE); - if (inSimulationModeParameter != null && inSimulationModeParameter.equals("true")) { - inSimulationMode = true; - this.speedSinWave = new SinWave(50, 8); - this.directionSinWave = new SinWave(100, 8); - } else { - inSimulationMode = false; - } movingAverageSpeed = new MovingAverage(500); movingAverageDirection = new MovingAverage(500); trueWindSpeedLiveAverageComponent = new LiveAverageComponent(stringConstants.dashboardTrueWindSpeed(), stringConstants.dashboardTrueWindSpeedUnit()); @@ -168,30 +152,42 @@ public class WindBotComponent extends Composite implements HasWidgets, WindBotDa * */ @Override public void updateWindBotUI(WindInfoForRaceDTO windInfoForRaceDTO) { - logger.log(Level.INFO, "WindBotComponent with id "+windBotId+" got notified about new WindInfoForRaceDTO"); + logger.log(Level.INFO, "WindBotComponent with id " + windBotId + " got notified about new WindInfoForRaceDTO"); if (windInfoForRaceDTO != null) { - WindTrackInfoDTO windTrackInfoDTO = getWindTrackInfoDTOFromAndWindBotID(windInfoForRaceDTO, windBotId); + final WindTrackInfoDTO windTrackInfoDTO = getWindTrackInfoDTOFromAndWindBotID(windInfoForRaceDTO, windBotId); if (windTrackInfoDTO != null) { - logger.log(Level.INFO, "WindInfoForRaceDTO contains WindTrackInfoDTO for Windbot id "+ windBotId); + logger.log(Level.INFO, "WindInfoForRaceDTO contains WindTrackInfoDTO for Windbot id " + windBotId); if (windTrackInfoDTO.windFixes != null) { if (windTrackInfoDTO.windFixes.size() > 0) { - logger.log(Level.INFO, "Upating UI with Wind Fixes for WindBot id "+ windBotId); - Point[] speedPoints = convertWindFixListIntoPointsArray(windTrackInfoDTO.windFixes, WindType.SPEED); - Point[] directionPoints = convertWindFixListIntoPointsArray(windTrackInfoDTO.windFixes, - WindType.DIRECTION); + logger.log(Level.INFO, "Upating UI with Wind Fixes for WindBot id " + windBotId); + SplitScheduler.get().schedule(new ScheduledCommand() { - trueWindSpeedLiveAverageComponent.setLiveValue(""+NumberFormat.getFormat("#0.0").format(speedPoints[speedPoints.length - 1].getY().doubleValue())); - trueWindSpeedLiveAverageComponent.setAverageValue(""+NumberFormat.getFormat("#0.0").format(movingAverageSpeed.getAverage())); - - trueWindDirectionLiveAverageComponent.setLiveValue(""+NumberFormat.getFormat("#0.0").format(directionPoints[directionPoints.length - 1].getY().doubleValue())); - trueWindDirectionLiveAverageComponent.setAverageValue(""+NumberFormat.getFormat("#0.0").format(movingAverageDirection.getAverage())); + @Override + public void execute() { + updateMovingAverages(windTrackInfoDTO.windFixes); + } + }); + SplitScheduler.get().schedule(new ScheduledCommand() { - trueWindSpeedVerticalWindChart.addPointsToSeriesWithAverage(speedPoints, - movingAverageSpeed.getAverage()); - trueWindDirectionVerticalWindChart.addPointsToSeriesWithAverage(directionPoints, - movingAverageDirection.getAverage()); - locationPointerCompass.windBotPositionChanged(windTrackInfoDTO.windFixes - .get(windTrackInfoDTO.windFixes.size() - 1).position); + @Override + public void execute() { + updateWindSpeedLabelsAndChart(windTrackInfoDTO); + } + }); + SplitScheduler.get().schedule(new ScheduledCommand() { + + @Override + public void execute() { + updateWindDirectionLabelsAndChart(windTrackInfoDTO); + } + }); + SplitScheduler.get().schedule(new ScheduledCommand() { + + @Override + public void execute() { + updateLocationPointerCompass(windTrackInfoDTO); + } + }); } else { logger.log(Level.INFO, "WindTrackInfoDTO.windFixes is empty"); } @@ -199,52 +195,37 @@ public class WindBotComponent extends Composite implements HasWidgets, WindBotDa logger.log(Level.INFO, "WindTrackInfoDTO.windFixes is null"); } } else { - logger.log(Level.INFO, "WindInfoForRaceDTO does not contains WindTrackInfoDTO for Windbot id "+ windBotId); + logger.log(Level.INFO, "WindInfoForRaceDTO does not contains WindTrackInfoDTO for Windbot id " + + windBotId); } } else { logger.log(Level.INFO, "WindInfoForRaceDTO is null"); } } - - private Point[] convertWindFixListIntoPointsArray(List windFixes, WindType windtype) { - if (windFixes != null && windFixes.size() > 0) { - Point[] points = new Point[windFixes.size()]; - int counter = 0; - if (windtype.equals(WindType.DIRECTION)) { - Point previouspoint = new Point(windFixes.get(0).requestTimepoint, windFixes.get(0).trueWindFromDeg); - for (WindDTO windDTO : windFixes) { - double nextSinusValue = 0; - if (inSimulationMode == true) { - nextSinusValue = directionSinWave.getNexNumber(); - } - points[counter] = new Point(windDTO.requestTimepoint, windDTO.trueWindFromDeg + nextSinusValue); - points[counter] = adaptWindDirectionPointToStayCloseToLastPoint(previouspoint, points[counter]); - previouspoint = points[counter]; - movingAverageDirection.add(windDTO.trueWindFromDeg + nextSinusValue); - counter++; - } - } else if (windtype.equals(WindType.SPEED)) { - Point previouspoint = new Point(windFixes.get(0).requestTimepoint, - windFixes.get(0).trueWindSpeedInKnots); - for (WindDTO windDTO : windFixes) { - double nextSinusValue = 0; - if (inSimulationMode == true) { - nextSinusValue = speedSinWave.getNexNumber(); - } - points[counter] = new Point(windDTO.requestTimepoint, windDTO.trueWindSpeedInKnots + nextSinusValue); - points[counter] = adaptWindDirectionPointToStayCloseToLastPoint(previouspoint, points[counter]); - previouspoint = points[counter]; - movingAverageSpeed.add(windDTO.trueWindSpeedInKnots + nextSinusValue); - counter++; - } - } - return points; - } else { - return null; + + private void updateWindSpeedLabelsAndChart(final WindTrackInfoDTO windTrackInfoDTO) { + int size = windTrackInfoDTO.windFixes.size(); + trueWindSpeedLiveAverageComponent.setLiveValue(""+NumberFormat.getFormat("#0.0").format(windTrackInfoDTO.windFixes.get(size-1).trueWindSpeedInKnots)); + trueWindSpeedVerticalWindChart.addPointsToSeriesWithAverageAndWindType(windTrackInfoDTO.windFixes, movingAverageSpeed.getAverage(), WindType.SPEED); + trueWindSpeedLiveAverageComponent.setAverageValue(""+NumberFormat.getFormat("#0.0").format(movingAverageSpeed.getAverage())); + } + + private void updateWindDirectionLabelsAndChart(final WindTrackInfoDTO windTrackInfoDTO) { + int size = windTrackInfoDTO.windFixes.size(); + trueWindDirectionLiveAverageComponent.setLiveValue(""+NumberFormat.getFormat("#0.0").format(windTrackInfoDTO.windFixes.get(size-1).trueWindFromDeg)); + trueWindDirectionVerticalWindChart.addPointsToSeriesWithAverageAndWindType(windTrackInfoDTO.windFixes, movingAverageDirection.getAverage(), WindType.DIRECTION); + trueWindDirectionLiveAverageComponent.setAverageValue(""+NumberFormat.getFormat("#0.0").format(movingAverageDirection.getAverage())); + } + + private void updateLocationPointerCompass(final WindTrackInfoDTO windTrackInfoDTO) { + locationPointerCompass.windBotPositionChanged(windTrackInfoDTO.windFixes + .get(windTrackInfoDTO.windFixes.size() - 1).position); + } + + private void updateMovingAverages(List windFixes) { + for(WindDTO windDTO : windFixes) { + movingAverageSpeed.add(windDTO.trueWindSpeedInKnots); + movingAverageDirection.add(windDTO.trueWindFromDeg); } } - - private Point adaptWindDirectionPointToStayCloseToLastPoint(Point previousPoint, Point point) { - return ChartPointRecalculator.stayClosestToPreviousPoint(previousPoint, point); - } } \ No newline at end of file From 40bb7ea1f3c1c9ad2bac1142eb261bb24f1dc527 Mon Sep 17 00:00:00 2001 From: Alexander Ries Date: Fri, 11 Dec 2015 10:22:21 +0100 Subject: [PATCH 153/306] DashboardDispatchSystemImpl uses now SplitScheduler to handle action callbacks --- .../gwt/shared/dispatch/impl/DashboardDispatchSystemImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchSystemImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchSystemImpl.java index 0e9a9986656..67bdb57fdb3 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchSystemImpl.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dispatch/impl/DashboardDispatchSystemImpl.java @@ -8,6 +8,6 @@ import com.sap.sailing.gwt.dispatch.client.impl.DispatchSystemImpl; public class DashboardDispatchSystemImpl extends DispatchSystemImpl implements DashboardDispatchSystem { public DashboardDispatchSystemImpl() { - super(RemoteServiceMappingConstants.ribdashboardDispatchServiceRemotePath); + super(RemoteServiceMappingConstants.ribdashboardDispatchServiceRemotePath, true); } } From 6e6bdc045d7d6488186c020eab0eee1ec4b51fa7 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Fri, 11 Dec 2015 13:00:39 +0100 Subject: [PATCH 154/306] test case for automatic selection of linked tracked races --- ...martphoneTrackingEventManagementPanel.java | 1 + ...martphoneTrackingEventManagementPanel.java | 159 ++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/test/adminconsole/TestSmartphoneTrackingEventManagementPanel.java diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java index 84128ce0ca2..83568db923f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SmartphoneTrackingEventManagementPanel.java @@ -332,6 +332,7 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC trackWind = new CheckBox(stringMessages.trackWind()); correctWindDirectionForDeclination = new CheckBox(stringMessages.declinationCheckbox()); startTrackingButton = new Button(stringMessages.startTracking()); + startTrackingButton.ensureDebugId("StartTrackingButton"); startTrackingButton.setEnabled(false); racesPanel.add(trackWind); racesPanel.add(correctWindDirectionForDeclination); diff --git a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/test/adminconsole/TestSmartphoneTrackingEventManagementPanel.java b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/test/adminconsole/TestSmartphoneTrackingEventManagementPanel.java new file mode 100644 index 00000000000..ac7d611f044 --- /dev/null +++ b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/test/adminconsole/TestSmartphoneTrackingEventManagementPanel.java @@ -0,0 +1,159 @@ +package com.sap.sailing.selenium.test.adminconsole; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.selenium.pages.adminconsole.AdminConsolePage; +import com.sap.sailing.selenium.pages.adminconsole.connectors.SmartphoneTrackingEventManagementPanelPO; +import com.sap.sailing.selenium.pages.adminconsole.leaderboard.LeaderboardConfigurationPanelPO; +import com.sap.sailing.selenium.pages.adminconsole.leaderboard.LeaderboardDetailsPanelPO; +import com.sap.sailing.selenium.pages.adminconsole.leaderboard.LeaderboardDetailsPanelPO.RaceDescriptor; +import com.sap.sailing.selenium.pages.adminconsole.regatta.RegattaDetailsCompositePO; +import com.sap.sailing.selenium.pages.adminconsole.regatta.RegattaStructureManagementPanelPO; +import com.sap.sailing.selenium.pages.adminconsole.regatta.SeriesEditDialogPO; +import com.sap.sailing.selenium.pages.adminconsole.regatta.RegattaListCompositePO.RegattaDescriptor; +import com.sap.sailing.selenium.pages.adminconsole.tracking.RaceColumnTableWrapperPO; +import com.sap.sailing.selenium.pages.adminconsole.tracking.TrackedRacesListPO; +import com.sap.sailing.selenium.pages.adminconsole.tracking.TrackedRacesListPO.Status; +import com.sap.sailing.selenium.pages.adminconsole.tracking.TrackedRacesListPO.TrackedRaceDescriptor; +import com.sap.sailing.selenium.pages.adminconsole.tractrac.TracTracEventManagementPanelPO; +import com.sap.sailing.selenium.pages.adminconsole.tractrac.TracTracEventManagementPanelPO.TrackableRaceDescriptor; +import com.sap.sailing.selenium.pages.gwt.CellTablePO; +import com.sap.sailing.selenium.pages.gwt.DataEntryPO; +import com.sap.sailing.selenium.pages.leaderboard.LeaderboardPage; +import com.sap.sailing.selenium.pages.leaderboard.LeaderboardTablePO; +import com.sap.sailing.selenium.test.AbstractSeleniumTest; + +public class TestSmartphoneTrackingEventManagementPanel extends AbstractSeleniumTest { + private static final String IDM_5O5_2013_JSON_URL = + "http://traclive.dk/events/event_20130917_IDMO/jsonservice.php"; //$NON-NLS-1$ + + private static final String REGATTA = "IDM 2013"; //$NON-NLS-1$ + + private static final String LEADERBOARD = "IDM 2013 (5O5)"; //$NON-NLS-1$ + + private static final String EVENT = "IDM 5O5 2013"; //$NON-NLS-1$ + private static final String BOAT_CLASS = "5O5"; //$NON-NLS-1$ + private static final String RACE = "Race %d"; //$NON-NLS-1$ + + private RegattaDescriptor regatta; + + private List trackableRaces; + private List trackedRaces; + private List leaderboardRaces; + + @Override + @Before + public void setUp() { + this.regatta = new RegattaDescriptor(REGATTA, BOAT_CLASS); + this.trackableRaces = new ArrayList<>(); + this.trackedRaces = new ArrayList<>(); + this.leaderboardRaces = new ArrayList<>(); + for (int i = 1; i <= 5; i++) { + String raceName = String.format(RACE, i); + TrackableRaceDescriptor trackableRace = new TrackableRaceDescriptor(EVENT, raceName, BOAT_CLASS); + TrackedRaceDescriptor trackedRace = new TrackedRaceDescriptor(this.regatta.toString(), BOAT_CLASS, raceName); + RaceDescriptor leaderboardRace = new RaceDescriptor(String.format("R%s", i), "Default", false, false, 0); + + this.trackableRaces.add(trackableRace); + this.trackedRaces.add(trackedRace); + this.leaderboardRaces.add(leaderboardRace); + } + clearState(getContextRoot()); + super.setUp(); + configureLeaderboard(); + } + + @Test + public void testAutomaticSelectionOfLinkedRaceInRaceTable() { + LeaderboardPage leaderboard = LeaderboardPage.goToPage(getWebDriver(),getContextRoot(), LEADERBOARD, false); + LeaderboardTablePO leaderboardTable = leaderboard.getLeaderboardTable(); + List races = leaderboardTable.getRaceNames(); + + assertThat(races.size(), equalTo(5)); + assertThat(leaderboardTable.getEntries().size(), equalTo(28)); + AdminConsolePage adminConsole = AdminConsolePage.goToPage(getWebDriver(), getContextRoot()); + LeaderboardConfigurationPanelPO leaderboardConfigurationPanelPO = adminConsole.goToLeaderboardConfiguration(); + LeaderboardDetailsPanelPO leaderboardDetails = leaderboardConfigurationPanelPO.getLeaderboardDetails(this.regatta.toString()); + + for (int i = 0; i < 5; i++) { + leaderboardDetails.linkRace(this.leaderboardRaces.get(i), this.trackedRaces.get(i)); + } + + SmartphoneTrackingEventManagementPanelPO smartphoneTrackingPanel = adminConsole.goToSmartphoneTrackingPanel(); + CellTablePO leaderboards = smartphoneTrackingPanel.getLeaderboardTable(); + // select leaderboard + DataEntryPO entryToSelect = null; + for(DataEntryPO entry : leaderboards.getEntries()) { + if(entry.getColumnContent("Name") == LEADERBOARD) { + entryToSelect = entry; + break; + } + } + assertNotNull(entryToSelect); + leaderboards.selectEntry(entryToSelect); + + RaceColumnTableWrapperPO raceColumnTableWrapper = smartphoneTrackingPanel.getRaceColumnTableWrapper(); + CellTablePO raceColumnTable = raceColumnTableWrapper.getRaceColumnTable(); + // select RaceColumn + DataEntryPO raceToSelect = null; + for (DataEntryPO entry : raceColumnTable.getEntries()) { + if (entry.getColumnContent("Race") == leaderboardRaces.get(0).getName()) { + raceToSelect = entry; + } + } + assertNotNull(raceToSelect); + raceColumnTable.selectEntry(raceToSelect); + + TrackedRacesListPO trackedRaces = smartphoneTrackingPanel.getTrackedRaceListComposite(); + List seleceted = trackedRaces.getTrackedRacesTable().getSelectedEntries(); + assertEquals(seleceted.size(),1); + assertEquals(this.trackedRaces.get(0).race, seleceted.get(0).getColumnContent("Race")); + } + + private void configureLeaderboard() { + // Open the admin console for some configuration steps + AdminConsolePage adminConsole = AdminConsolePage.goToPage(getWebDriver(), getContextRoot()); + + // Create a regatta with 1 series and 5 races as well as a leaderborad + RegattaStructureManagementPanelPO regattaStructure = adminConsole.goToRegattaStructure(); + regattaStructure.createRegatta(this.regatta); + + RegattaDetailsCompositePO regattaDetails = regattaStructure.getRegattaDetails(this.regatta); + SeriesEditDialogPO seriesDialog = regattaDetails.editSeries(RegattaStructureManagementPanelPO.DEFAULT_SERIES_NAME); + seriesDialog.addRaces(1, 5); + seriesDialog.pressOk(); + + LeaderboardConfigurationPanelPO leaderboardConfiguration = adminConsole.goToLeaderboardConfiguration(); + leaderboardConfiguration.createRegattaLeaderboard(this.regatta); + + // Start the tracking for the races and wait until they are ready to use + TracTracEventManagementPanelPO tracTracEvents = adminConsole.goToTracTracEvents(); + tracTracEvents.listTrackableRaces(IDM_5O5_2013_JSON_URL); + tracTracEvents.setReggataForTracking(this.regatta); + tracTracEvents.setTrackSettings(false, false, false); + // TODO: There exists a bug in Selenium with key modifiers (Issue 3734 and 6817), so we can't use multi + // selection (Firefox on Windows) + //tracTracEvents.startTrackingForRaces(this.trackableRaces); + for(TrackableRaceDescriptor race : this.trackableRaces) { + tracTracEvents.startTrackingForRace(race); + } + + TrackedRacesListPO trackedRacesList = tracTracEvents.getTrackedRacesList(); + trackedRacesList.waitForTrackedRaces(this.trackedRaces, Status.FINISHED); // TracAPI puts REPLAY races into FINISHED mode when done loading + // TODO: There exists a bug in Selenium with key modifiers (Issue 3734 and 6817), so we can't use multi + // selection (Firefox on Windows) + //trackedRacesList.stopTracking(this.trackedRaces); + for (TrackedRaceDescriptor race : this.trackedRaces) { + trackedRacesList.stopTracking(race); + } + } +} \ No newline at end of file From 0d9042c0e570952ae75b4a15c477dad2ae030f47 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 11 Dec 2015 13:55:43 +0100 Subject: [PATCH 155/306] upgraded TracAPI to 3.1.0, adding TracAPI-src.jar which provides the sources for the API Change-Id: I4042b610309edde8b57f95384a4c17435688bfb6 --- java/com.tractrac.clientmodule/.classpath | 24 +++++----- java/com.tractrac.clientmodule/README.txt | 41 ++++++++++++++++++ .../lib/TracAPI-src.jar | Bin 0 -> 61614 bytes .../com.tractrac.clientmodule/lib/TracAPI.jar | Bin 783263 -> 783819 bytes 4 files changed, 53 insertions(+), 12 deletions(-) create mode 100755 java/com.tractrac.clientmodule/lib/TracAPI-src.jar diff --git a/java/com.tractrac.clientmodule/.classpath b/java/com.tractrac.clientmodule/.classpath index d85a01f6fba..91a9c24eec3 100644 --- a/java/com.tractrac.clientmodule/.classpath +++ b/java/com.tractrac.clientmodule/.classpath @@ -1,12 +1,12 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/java/com.tractrac.clientmodule/README.txt b/java/com.tractrac.clientmodule/README.txt index 13c1e81daff..fef732815ec 100644 --- a/java/com.tractrac.clientmodule/README.txt +++ b/java/com.tractrac.clientmodule/README.txt @@ -15,6 +15,47 @@ It contains also some files: - test.sh -> script that compiles the code in the src folder, creates the test.jar library and execute the code of the example. - Manifest.txt -> manifest used to create the test.jar file + +******************************************** +************* TracAPI 3.1.0 **************** +******************************************** +This is a final version. It fixes bugs in the implementation and it adds some new features. +These features add methods to the API breaking the backward compatibility. These changes are: + + - The IRaceCompetitorListener interface implements a new method removeOffsetPositions. If + your app implements this interface, you have to implement this extra method. + +This version provides a new JavaDoc version. + +Release date: 11/12/2015 +Build number: 11801 + + 1) Features + + - Adding the method IRaceCompetitorListener.removeOffsetPositions that is invoked + to invalidate a set of positions. It is used when the event administrator detects + an error in the snapping algorithm (in route sports) and he want to remove the wrong + positions (Requested by Chris Terkelsen, 26/11/2015) + + - Adding the method IControl.getMapName() that contains the name that has to be used to + display the control on the map (Requested by Chris Terkelsen, 09/12/2015) + + 2) Bugs + + - The static controls send a position event using the race start time instead the + event start time. (Reported by Jerome Soussens, 11/12/2015) + +******************************************** +************* TracAPI 3.0.14 *************** +******************************************** +This is a final version. Only fixes bugs in the implementation + +Release date: 29/09/2015 +Build number: 11514 + + 1) Bugs + + - NullPointerException caused by a synchronization error (Reported by Jérome Soussens, 28/09/2015) ******************************************** ************* TracAPI 3.0.13 *************** diff --git a/java/com.tractrac.clientmodule/lib/TracAPI-src.jar b/java/com.tractrac.clientmodule/lib/TracAPI-src.jar new file mode 100755 index 0000000000000000000000000000000000000000..19863a47498b57a4af53e76359aa913eb6b9692b GIT binary patch literal 61614 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

    Z-Er)14p41r60?g3~P_Xt<; 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 uKZIX7i{Zu88Xsc)OzgO2=dbuBLd delta 91586 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 From 9c3d6178b9c3f654f849a4e89f86e19dba2fd325 Mon Sep 17 00:00:00 2001 From: Lukas Furmanek Date: Fri, 11 Dec 2015 14:30:25 +0100 Subject: [PATCH 156/306] newSelectionModel: added an EntityIdentityComparator --- .../sailing/gwt/ui/adminconsole/RaceTableWrapper.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java index 388adf0c6af..e4ccdfd98e7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceTableWrapper.java @@ -18,6 +18,7 @@ import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sse.common.util.NaturalComparator; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.celltable.EntityIdentityComparator; import com.sap.sse.gwt.client.celltable.RefreshableSelectionModel; public class RaceTableWrapper> @@ -27,7 +28,15 @@ extends TableWrapper { public RaceTableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter errorReporter, boolean multiSelection) { - super(sailingService, stringMessages, errorReporter, multiSelection, /* enablePager */ false, null /*EntityIdentityComparator for RefreshableSelectionModel*/); + super(sailingService, stringMessages, errorReporter, multiSelection, /* enablePager */ false, + new EntityIdentityComparator() { + @Override + public boolean representSameEntity(RaceColumnDTOAndFleetDTOWithNameBasedEquality dto1, + RaceColumnDTOAndFleetDTOWithNameBasedEquality dto2) { + return dto1.getA().getName().toString().equals(dto2.getA().getName().toString()) && + dto1.getB().getName().toString().equals(dto2.getB().getName().toString()); + } + }); Column raceNameColumn = new Column(new AnchorCell()) { @Override From 3b6ac414dc0f4f2289c688e1896a2c12973cb0f0 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 11 Dec 2015 16:16:55 +0100 Subject: [PATCH 157/306] some minor refactorings; adding Javadoc Change-Id: I6280cd66388350778841b1efcf6fb439cce50b05 --- .../shared/racemap/AdvantageLineAnimator.java | 95 +++++++++++++------ .../gwt/ui/client/shared/racemap/RaceMap.java | 25 ++--- 2 files changed, 80 insertions(+), 40 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java index 10a2acee30f..098fbadf186 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/AdvantageLineAnimator.java @@ -5,59 +5,96 @@ import com.google.gwt.maps.client.mvc.MVCArray; import com.google.gwt.maps.client.overlays.Polyline; import com.google.gwt.user.client.Timer; -class AdvantageLineAnimator extends Timer { - private static final int ANIMATION_PERIOD = 100; +/** + * Moves a {@link Polyline} from its current position to a target position in a given duration with a fixed + * {@link #ANIMATION_PERIOD_MILLIS animation interval}. Since the timer may be triggered at non-exact intervals, the + * offset per animation tick is calculated based on the current time for each iteration. + *

    + * + * If an animation is still going on, it is permissible for clients to invoke + * {@link #setNextPositionAndTransitionMillis(MVCArray, long)} which will have the effect that from its current position + * reached so far the {@link Polyline} will then be animated to the next target position in the specified time interval. + * + * @author Jonas Dann + * + */ +public class AdvantageLineAnimator extends Timer { + /** + * Time between two animation steps + */ + private static final int ANIMATION_PERIOD_MILLIS = 100; + /** + * The polyline to animate by updating its {@link Polyline#getPath() path} + */ private final Polyline advantageLine; + + /** + * The last time the coordinates in the {@link #advantageLine}'s path were updated + */ private long lastTime; + + /** + * The remaining time for the animation to move the {@link #advantageLine} to its {@link #nextPosition target + * position}. If -1, the {@link #advantageLine} will be moved to its {@link #nextPosition next target + * position} in one step. + */ private long durationMillis = -1; + + /** + * The "to-be" / target position to which to move the {@link #advantageLine}'s path gradually during the remaining + * {@link #durationMillis}. + */ private MVCArray nextPosition; public AdvantageLineAnimator(Polyline advantageLine) { + if (advantageLine == null) { + throw new NullPointerException("Advantage line must not be null"); + } this.advantageLine = advantageLine; } + /** + * @param timeForPositionTransitionMillis if -1 then no animation is requested and the {@link #advantageLine} will + * be moved to its {@link #nextPosition next target position} in one step. + * + * @param nextPosition the next target position for the {@link #advantageLine}; if null, no animation will happen at all + */ public void setNextPositionAndTransitionMillis(MVCArray nextPosition, long timeForPositionTransitionMillis) { + if (nextPosition.getLength() != advantageLine.getPath().getLength()) { + throw new IllegalArgumentException("The next animation position for a polyline with "+advantageLine.getPath().getLength()+ + " elements must have the same number of positions but did have "+nextPosition.getLength()); + } this.nextPosition = nextPosition; - scheduleRepeating(ANIMATION_PERIOD, timeForPositionTransitionMillis); + this.lastTime = System.currentTimeMillis(); + this.durationMillis = timeForPositionTransitionMillis; + this.scheduleRepeating(ANIMATION_PERIOD_MILLIS); } - public void scheduleRepeating(int periodMillis, long durationMillis) { - this.lastTime = System.currentTimeMillis(); - this.scheduleRepeating(periodMillis); - this.durationMillis = durationMillis; - } - @Override public void run() { - long time = System.currentTimeMillis(); - long deltaTime = time - lastTime; - lastTime = time; - double moveFactor = 1; + final long time = System.currentTimeMillis(); + final double moveFactor; // how much of the delta to the nextPosition shall this tick move the advantage line? if (durationMillis > 0) { - long oldDurationMillis = durationMillis; + long deltaTime = time - lastTime; + moveFactor = Math.min(1.0, (double) deltaTime / (double) durationMillis); // don't overshoot durationMillis -= deltaTime; - if (durationMillis < 0) { - deltaTime = oldDurationMillis; - durationMillis = 0; - } - moveFactor = (double)deltaTime / oldDurationMillis; - } else if (durationMillis == 0) { - this.cancel(); - return; + } else { + moveFactor = 1; } - if (advantageLine != null && nextPosition != null) { + if (nextPosition != null) { final MVCArray currentPosition = this.advantageLine.getPath(); - - LatLng currentLatLng; - LatLng nextLatLng; - for(int i = 0; i <= 1; i++) { - currentLatLng = currentPosition.get(i); - nextLatLng = nextPosition.get(i); + for (int i = 0; i < nextPosition.getLength(); i++) { + final LatLng currentLatLng = currentPosition.get(i); + final LatLng nextLatLng = nextPosition.get(i); currentPosition.setAt(i, LatLng.newInstance(currentLatLng.getLatitude() + (nextLatLng.getLatitude() - currentLatLng.getLatitude()) * moveFactor, currentLatLng.getLongitude() + (nextLatLng.getLongitude() - currentLatLng.getLongitude()) * moveFactor)); } } + if (durationMillis <= 0) { + cancel(); + } + lastTime = time; } public void removeAnimation() { 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 cd0466872b1..0c22d286786 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 @@ -176,7 +176,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe /** * Polyline for the advantage line (the leading line for the boats, orthogonal to the wind direction; touching the leading boat). */ - Polyline advantageLine; + private Polyline advantageLine; private AdvantageLineAnimator advantageTimer; @@ -714,7 +714,9 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe infoOverlay.removeCanvasPositionAndRotationTransition(); } // remove the advantage line animation - advantageTimer.removeAnimation(); + if (advantageTimer != null) { + advantageTimer.removeAnimation(); + } } public void redraw() { @@ -771,6 +773,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe 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()); int requestID = ++boatPositionRequestIDCounter; @@ -780,7 +783,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe GetRaceMapDataAction getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping = getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping(fromAndToAndOverlap, race, newTime); if (getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping != null) { asyncActionsExecutor.execute(getRaceMapDataForAllOverlappingAndTipsOfNonOverlapping, GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); + getRaceMapDataCallback(newTime, transitionTimeInMillis, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); requestID = ++boatPositionRequestIDCounter; } // next, do the full thing; being the later call, if request throttling kicks in, the later call @@ -788,7 +791,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe GetRaceMapDataAction getRaceMapDataAction = new GetRaceMapDataAction(sailingService, competitorSelection.getAllCompetitors(), race, useNullAsTimePoint() ? null : newTime, fromAndToAndOverlap.getA(), fromAndToAndOverlap.getB(), /* extrapolate */ true, (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null)); asyncActionsExecutor.execute(getRaceMapDataAction, GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(oldTime, newTime, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); + getRaceMapDataCallback(newTime, transitionTimeInMillis, fromAndToAndOverlap.getC(), competitorsToShow, requestID)); // draw the wind into the map, get the combined wind List windSourceTypeNames = new ArrayList(); @@ -807,7 +810,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe List> windSourcesToShow = new ArrayList>(); if (windInfo != null) { lastCombinedWindTrackInfoDTO = windInfo; -// showAdvantageLine(competitorsToShow, newTime); // TODO bug 1662: can we live without this call and assume that updating the advantage line in case the boats are moved is enough? + showAdvantageLine(competitorsToShow, newTime, transitionTimeInMillis); for (WindSource windSource : windInfo.windTrackInfoByWindSource.keySet()) { WindTrackInfoDTO windTrackInfoDTO = windInfo.windTrackInfoByWindSource.get(windSource); switch (windSource.getType()) { @@ -874,8 +877,8 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe } private AsyncCallback getRaceMapDataCallback( - final Date oldTime, final Date newTime, + final long transitionTimeInMillis, final Map hasTailOverlapForCompetitor, final Iterable competitorsToShow, final int requestID) { return new AsyncCallback() { @@ -899,10 +902,9 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe startedProcessingRequestID = requestID; // Do boat specific actions Map> boatData = raceMapDataDTO.boatPositions; - long timeForPositionTransitionMillis = calculateTimeForPositionTransitionInMillis(newTime, oldTime); - fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, timeForPositionTransitionMillis); - showBoatsOnMap(newTime, timeForPositionTransitionMillis, getCompetitorsToShow()); - showCompetitorInfoOnMap(newTime, timeForPositionTransitionMillis, competitorSelection.getSelectedFilteredCompetitors()); + fixesAndTails.updateFixes(boatData, hasTailOverlapForCompetitor, RaceMap.this, transitionTimeInMillis); + showBoatsOnMap(newTime, transitionTimeInMillis, getCompetitorsToShow()); + showCompetitorInfoOnMap(newTime, transitionTimeInMillis, competitorSelection.getSelectedFilteredCompetitors()); if (douglasMarkers != null) { removeAllMarkDouglasPeuckerpoints(); } @@ -920,7 +922,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe showStartLineToFirstMarkTriangle(raceMapDataDTO.coursePositions); // 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, timeForPositionTransitionMillis); + showAdvantageLine(competitorsToShow, newTime, transitionTimeInMillis); // Rezoom the map LatLngBounds zoomToBounds = null; @@ -1337,6 +1339,7 @@ public class RaceMap extends AbsolutePanel implements TimeListener, CompetitorSe if (advantageLine != null) { advantageLine.setMap(null); advantageLine = null; + advantageTimer = null; } } } From 299099bd0d8f74aeeaf0187cf215eed46dc4f9ec Mon Sep 17 00:00:00 2001 From: Jonas Dann Date: Fri, 11 Dec 2015 16:26:07 +0100 Subject: [PATCH 158/306] Added the animated advantage line to the Whats New Notes --- .../gwt/home/solutions/SailingAnalyticsNotes.html | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html index 69c32d47aea..6c0d43b7814 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/home/solutions/SailingAnalyticsNotes.html @@ -7,12 +7,14 @@

    December 2015