From 39ea54f73ae3ca862d0cffdc077a523e8bdb74a2 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 24 Jul 2018 12:58:45 +0200 Subject: [PATCH 01/58] bug4483 Added a label to the number of races on to of the progress bars --- .../partials/multiregattalist/MultiRegattaListStepsBody.java | 4 ++-- .../gwt/home/mobile/partials/eventsteps/EventStepsPhase.java | 4 ++-- .../java/com/sap/sailing/gwt/ui/client/StringMessages.java | 2 +- .../com/sap/sailing/gwt/ui/client/StringMessages.properties | 3 ++- .../sap/sailing/gwt/ui/client/StringMessages_de.properties | 3 ++- .../sap/sailing/gwt/ui/client/StringMessages_es.properties | 1 - .../sap/sailing/gwt/ui/client/StringMessages_fr.properties | 1 - .../sap/sailing/gwt/ui/client/StringMessages_ja.properties | 1 - .../sap/sailing/gwt/ui/client/StringMessages_pt.properties | 1 - .../sap/sailing/gwt/ui/client/StringMessages_ru.properties | 1 - .../sap/sailing/gwt/ui/client/StringMessages_zh.properties | 1 - 11 files changed, 9 insertions(+), 13 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java index f1ce515f0bb..a2dc107749d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java @@ -49,10 +49,10 @@ public class MultiRegattaListStepsBody extends UIObject implements RequiresResiz mediumNameDummyUi.setInnerText(seriesNameMedium = caculateSeriesNameMedium()); seriesNameShort = caculateSeriesNameShort(); if (seriesProgress.isCompleted()) { - progressUi.setInnerText(String.valueOf(seriesProgress.getTotalRaceCount())); + progressUi.setInnerText(I18N.racesCount(seriesProgress.getTotalRaceCount())); } else { checkUi.getStyle().setDisplay(Display.NONE); - progressUi.setInnerText(I18N.currentOfTotal( + progressUi.setInnerText(I18N.currentOfTotalRaces( seriesProgress.getProgressRaceCount(), seriesProgress.getTotalRaceCount())); } addFleetProgresses(seriesProgress.getFleetState(), seriesProgress.getMaxRacesPerFleet()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java index 058d4bd75b9..3480d6fc5d0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java @@ -35,11 +35,11 @@ public class EventStepsPhase extends Composite { initWidget(uiBinder.createAndBindUi(this)); nameUi.setInnerText(DEFAULT_SERIES_NAME.equals(seriesProgress.getName()) ? I18N.races() : seriesProgress.getName()); if (seriesProgress.isCompleted()) { - progressUi.setInnerText(String.valueOf(seriesProgress.getTotalRaceCount())); + progressUi.setInnerText(I18N.racesCount(seriesProgress.getTotalRaceCount())); } else { checkUi.getStyle().setDisplay(Display.NONE); int current = seriesProgress.getProgressRaceCount(), total = seriesProgress.getTotalRaceCount(); - progressUi.setInnerText(I18N.currentOfTotal(current, total)); + progressUi.setInnerText(I18N.currentOfTotalRaces(current, total)); } if (seriesProgress.getProgressRaceCount() == 0) { addStyleName(EventStepsResources.INSTANCE.css().eventsteps_phases_phaseinactive()); 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 cc7951e05cc..f2ef38ed9ab 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -1441,7 +1441,7 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String courseGeometry(); String degreesToWind(String diff); String miniLeeaderboardLegendText(); - String currentOfTotal(int current, int total); + String currentOfTotalRaces(int current, @PluralCount(DefaultRule_1_0n.class) int total); String noFinishedRaces(); String racesOverview(); String listFormatLabel(); 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 8dd85fc710c..107537d2c53 100755 --- 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 @@ -1428,7 +1428,8 @@ numberOfRacesShort=# Races courseGeometry=Course Geometry degreesToWind={0}° to wind miniLeeaderboardLegendText=Shows the points along with the number of races per competitor -currentOfTotal={0,number}/{1,number} +currentOfTotalRaces={0,number}/{1,number} races +currentOfTotalRaces[one]={0,number}/{1,number} race noFinishedRaces=There are no finished races yet racesOverview=Races overview listFormatLabel=List format 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 48ef82f8c55..8227d8dbaa6 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -1411,7 +1411,8 @@ numberOfRacesShort=# Rennen courseGeometry=Kursgeometrie degreesToWind={0}° zum Wind miniLeeaderboardLegendText=Es werden die Punkte und die Anzahl Rennen je Team angezeigt -currentOfTotal={0,number}/{1,number} +currentOfTotalRaces={0,number}/{1,number} Rennen +currentOfTotalRaces[one]={0,number}/{1,number} Rennen noFinishedRaces=Es wurden noch keine Wettfahrten beendet. racesOverview=Wettfahrtübersicht listFormatLabel=Listenansicht diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties index 50155bb02e9..0a4e37881d5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties @@ -1421,7 +1421,6 @@ numberOfRacesShort=Nº regatas courseGeometry=Geometría de la regata degreesToWind={0}° respecto al viento miniLeeaderboardLegendText=Muestra los puntos y el número de carreras por competidor -currentOfTotal={0,number}/{1,number} noFinishedRaces=Todavía no ha finalizado ninguna carrera racesOverview=Resumen de carreras listFormatLabel=Formato de lista diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties index cd84fe961fd..e8d14957744 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties @@ -1421,7 +1421,6 @@ numberOfRacesShort=Nombre de courses courseGeometry=Géométrie du parcours degreesToWind={0}° au vent miniLeeaderboardLegendText=Affiche les points associés au nombre de courses par concurrent -currentOfTotal={0,number}/{1,number} noFinishedRaces=Aucune course terminée pour l''instant. racesOverview=Synthèse des courses listFormatLabel=Format de liste diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties index fa4ad030dc2..328737b10bc 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties @@ -1421,7 +1421,6 @@ numberOfRacesShort=# レース courseGeometry=コース配置 degreesToWind=風と {0}° miniLeeaderboardLegendText=競技者別にレース数とともに得点を表示 -currentOfTotal={0,number}/{1,number} noFinishedRaces=フィニッシュしたレースがまだありません。 racesOverview=レース概要 listFormatLabel=レース一覧 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties index 5bad9ffc42c..5aa4add0f2f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties @@ -1421,7 +1421,6 @@ numberOfRacesShort=Nº de corridas courseGeometry=Geometria do percurso degreesToWind={0}° para vento miniLeeaderboardLegendText=Exibe os pontos juntamente com o número de corridas por competidor -currentOfTotal={0,number}/{1,number} noFinishedRaces=Ainda não existem corridas concluídas. racesOverview=Síntese de corridas listFormatLabel=Formato de lista 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 c2e3bbc1ab1..f8ee0825209 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 @@ -1421,7 +1421,6 @@ numberOfRacesShort=Гонки courseGeometry=Геометрия курса degreesToWind={0}° к ветру miniLeeaderboardLegendText=Показывает баллы и число гонок каждого участника -currentOfTotal={0,number}/{1,number} noFinishedRaces=Завершенных гонок еще нет racesOverview=Обзор гонок listFormatLabel=Формат списка 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 0c2450d869e..dfe289bd213 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 @@ -1421,7 +1421,6 @@ numberOfRacesShort=比赛轮次数 courseGeometry=场地几何结构 degreesToWind={0}°(相对于风) miniLeeaderboardLegendText=显示分数以及各参赛队的比赛轮次数 -currentOfTotal={0,number}/{1,number} noFinishedRaces=没有结束的比赛轮次 racesOverview=比赛轮次总览 listFormatLabel=清单格式 From 2a12da9b006071ef07d7ef746fdd9f32cca0c0ef Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 24 Jul 2018 17:51:12 +0200 Subject: [PATCH 02/58] bug4483 Optionally provide a preferred series' name when navigation to RegattaRacesPlace --- .../regatta/racestab/RegattaRacesPlace.java | 19 +++++++++++++++++-- .../RegattaRacesNavigationProvider.java | 2 ++ .../places/event/AbstractEventActivity.java | 6 ++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/racestab/RegattaRacesPlace.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/racestab/RegattaRacesPlace.java index 574760b72cb..4ae3f62a362 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/racestab/RegattaRacesPlace.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/event/regatta/racestab/RegattaRacesPlace.java @@ -1,5 +1,7 @@ package com.sap.sailing.gwt.home.desktop.places.event.regatta.racestab; +import java.util.Optional; + import com.google.gwt.place.shared.Prefix; import com.sap.sailing.gwt.home.desktop.places.event.regatta.AbstractEventRegattaPlace; import com.sap.sailing.gwt.home.shared.app.HasMobileVersion; @@ -8,24 +10,37 @@ import com.sap.sailing.gwt.home.shared.places.event.AbstractEventPlace; import com.sap.sailing.gwt.home.shared.places.event.EventContext; public class RegattaRacesPlace extends AbstractEventRegattaPlace implements HasMobileVersion { + + private final String preferredSeriesName; + public RegattaRacesPlace(String id, String leaderboardName) { super(id, leaderboardName); + this.preferredSeriesName = null; } public RegattaRacesPlace(EventContext context) { + this(context, null); + } + + public RegattaRacesPlace(EventContext context, String preferredSeriesName) { super(context); + this.preferredSeriesName = preferredSeriesName; + } + + public final Optional getPreferredSeriesName() { + return Optional.ofNullable(preferredSeriesName); } @Override public AbstractEventRegattaPlace newInstanceWithContext(EventContext ctx) { - return new RegattaRacesPlace(ctx); + return new RegattaRacesPlace(ctx, null); } @Prefix(PlaceTokenPrefixes.RegattaRaces) public static class Tokenizer extends AbstractEventPlace.Tokenizer { @Override protected RegattaRacesPlace getRealPlace(EventContext context) { - return new RegattaRacesPlace(context); + return new RegattaRacesPlace(context, null); } } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/RegattaRacesNavigationProvider.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/RegattaRacesNavigationProvider.java index d4f921b1a54..8c3655e1abf 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/RegattaRacesNavigationProvider.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/RegattaRacesNavigationProvider.java @@ -5,4 +5,6 @@ import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; public interface RegattaRacesNavigationProvider { PlaceNavigation getRegattaRacesNavigation(String regattaId); + + PlaceNavigation getRegattaRacesNavigation(String regattaId, String seriesName); } 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 0ac77756ff0..96e653ce512 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 @@ -247,6 +247,12 @@ public abstract class AbstractEventActivity ex EventContext ctx = new EventContext(getCtx()).withRegattaId(regattaId).withRegattaAnalyticsManager(null); return clientFactory.getNavigator().getEventNavigation(new RegattaRacesPlace(ctx), null, false); } + + @Override + public PlaceNavigation getRegattaRacesNavigation(String regattaId, String prefSeriesName) { + EventContext ctx = new EventContext(getCtx()).withRegattaId(regattaId).withRegattaAnalyticsManager(null); + return clientFactory.getNavigator().getEventNavigation(new RegattaRacesPlace(ctx, prefSeriesName), null, false); + } public PlaceNavigation getLatesNewsNavigation() { EventContext ctx = new EventContext(getCtx()).withRegattaId(null).withRegattaAnalyticsManager(null); From 2cdf3a578b851f1d4e3fe22229098ca812c1c8b0 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 24 Jul 2018 17:57:14 +0200 Subject: [PATCH 03/58] bug4483 Enabled navigation from series progresses to races page --- .../home/mobile/partials/eventsteps/EventSteps.gss | 3 +++ .../mobile/partials/eventsteps/EventSteps.java | 10 ++++++++-- .../partials/eventsteps/EventStepsPhase.java | 7 ++++++- .../partials/eventsteps/EventStepsPhase.ui.xml | 14 ++++++++------ .../partials/eventsteps/EventStepsResources.java | 1 + .../overview/regatta/RegattaOverviewImpl.java | 6 +++++- 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss index 282125f546d..7471b4468c8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss @@ -21,6 +21,9 @@ .eventsteps_phases_phaseinactive { color: #999; } +.eventsteps_phases_phase_link { + color: #000; +} /* .eventsteps_phases_phase:nth-child(2n-1) { */ /* padding-right: 0.333333333333333em; */ /* } */ diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java index 53cc73e4a0e..0f62c0792f3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java @@ -1,5 +1,7 @@ package com.sap.sailing.gwt.home.mobile.partials.eventsteps; +import java.util.function.Function; + import com.google.gwt.core.client.GWT; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.uibinder.client.UiBinder; @@ -11,6 +13,7 @@ import com.sap.sailing.gwt.home.communication.regatta.RegattaProgressSeriesDTO; import com.sap.sailing.gwt.home.communication.regatta.RegattaWithProgressDTO; import com.sap.sailing.gwt.home.mobile.partials.section.MobileSection; import com.sap.sailing.gwt.home.mobile.partials.sectionHeader.SectionHeaderContent; +import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.home.shared.refresh.RefreshableWidget; public class EventSteps extends Composite implements RefreshableWidget { @@ -22,8 +25,10 @@ public class EventSteps extends Composite implements RefreshableWidget> racesNavigationFactory; - public EventSteps(RegattaMetadataDTO regatta) { + public EventSteps(RegattaMetadataDTO regatta, Function> racesNavigationFactory) { + this.racesNavigationFactory = racesNavigationFactory; EventStepsResources.INSTANCE.css().ensureInjected(); initWidget(regattaProgessUi = uiBinder.createAndBindUi(this)); ImageResource boatClassIcon = BoatClassImageResolver.getBoatClassIconResource(regatta.getBoatClass()); @@ -37,7 +42,8 @@ public class EventSteps extends Composite implements RefreshableWidget navigation = racesNavigationFactory.apply(seriesProgress.getName()); + regattaProgessUi.addContent(new EventStepsPhase(seriesProgress, navigation)); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java index 3480d6fc5d0..141d2815cfb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java @@ -6,6 +6,7 @@ import java.util.Map; import java.util.Map.Entry; import com.google.gwt.core.client.GWT; +import com.google.gwt.dom.client.AnchorElement; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.ImageElement; import com.google.gwt.dom.client.Style.Display; @@ -16,7 +17,9 @@ import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.home.communication.race.FleetMetadataDTO; import com.sap.sailing.gwt.home.communication.regatta.RegattaProgressFleetDTO; import com.sap.sailing.gwt.home.communication.regatta.RegattaProgressSeriesDTO; +import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.gwt.client.LinkUtil; public class EventStepsPhase extends Composite { @@ -26,13 +29,15 @@ public class EventStepsPhase extends Composite { interface EventStepsPhaseUiBinder extends UiBinder { } + @UiField AnchorElement anchorUi; @UiField DivElement nameUi; @UiField ImageElement checkUi; @UiField DivElement progressUi; @UiField DivElement fleetsContainerUi; - public EventStepsPhase(RegattaProgressSeriesDTO seriesProgress) { + public EventStepsPhase(RegattaProgressSeriesDTO seriesProgress, PlaceNavigation placeNavigation) { initWidget(uiBinder.createAndBindUi(this)); + LinkUtil.configureForAction(anchorUi, () -> placeNavigation.goToPlace()); nameUi.setInnerText(DEFAULT_SERIES_NAME.equals(seriesProgress.getName()) ? I18N.races() : seriesProgress.getName()); if (seriesProgress.isCompleted()) { progressUi.setInnerText(I18N.racesCount(seriesProgress.getTotalRaceCount())); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.ui.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.ui.xml index 7cece0b816b..2ac9004e380 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.ui.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.ui.xml @@ -6,11 +6,13 @@ -
-
- -
-
-
+ +
+
+ +
+
+
+
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsResources.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsResources.java index 1bc6a0dfbd2..46e49f87206 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsResources.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsResources.java @@ -16,6 +16,7 @@ public interface EventStepsResources extends ClientBundle { String eventsteps_phases(); String eventsteps_phases_phase(); String eventsteps_phases_phaseinactive(); + String eventsteps_phases_phase_link(); String eventsteps_phases_phase_name(); String eventsteps_phases_phase_progressbar(); String eventsteps_phases_phase_progressbar_fleet(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/regatta/RegattaOverviewImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/regatta/RegattaOverviewImpl.java index 83403e8d3a8..afe0f4b5e58 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/regatta/RegattaOverviewImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/overview/regatta/RegattaOverviewImpl.java @@ -3,6 +3,7 @@ package com.sap.sailing.gwt.home.mobile.places.event.overview.regatta; import java.util.Collection; import java.util.Map; import java.util.Set; +import java.util.function.Function; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Panel; @@ -19,6 +20,7 @@ import com.sap.sailing.gwt.home.mobile.partials.quickfinder.Quickfinder; import com.sap.sailing.gwt.home.mobile.places.QuickfinderPresenter; import com.sap.sailing.gwt.home.mobile.places.event.EventViewBase; import com.sap.sailing.gwt.home.mobile.places.event.overview.AbstractEventOverview; +import com.sap.sailing.gwt.home.shared.app.PlaceNavigation; import com.sap.sailing.gwt.ui.client.FlagImageResolver; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -54,7 +56,9 @@ public class RegattaOverviewImpl extends AbstractEventOverview { } private void setupProgress(Panel container) { - eventStepsUi = new EventSteps(currentPresenter.getRegatta()); + final Function> racesNavigationFactory = prefSeriesName -> currentPresenter + .getRegattaRacesNavigation(getRegattaId(), prefSeriesName); + eventStepsUi = new EventSteps(currentPresenter.getRegatta(), racesNavigationFactory); refreshManager.add(eventStepsUi, new GetRegattaWithProgressAction(getEventId(), getRegattaId())); container.add(eventStepsUi); } From b217ae2f7b5e697df68cdacfba9fd826bf35c310 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 24 Jul 2018 18:01:06 +0200 Subject: [PATCH 04/58] bug4483 Added first version of util method to scroll an element to the top --- .../src/com/sap/sse/gwt/client/DOMUtils.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DOMUtils.java diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DOMUtils.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DOMUtils.java new file mode 100644 index 00000000000..639d68e0816 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DOMUtils.java @@ -0,0 +1,37 @@ +package com.sap.sse.gwt.client; + +import com.google.gwt.dom.client.Element; + +/** + * Utility class contained convenience methods regarding DOM operations. + */ +public final class DOMUtils { + + private DOMUtils() { + } + + /** + * Scrolls the top of the provided {@link Element element} to the top of the viewport. + * + * @param element + * {@link Element} to scroll to the top + */ + public static void scrollToTop(final Element element) { + int top = getOffsetTopOf(element); + Element current = element.getParentElement(); + while (current != null) { + if (top != current.getScrollTop()) { + current.setScrollTop(top); + } + top += getOffsetTopOf(current) - current.getScrollTop(); + current = current.getParentElement(); + } + } + + private static int getOffsetTopOf(final Element element) { + final int top = element.getOffsetTop(); + final boolean parentIsOffsetParent = element.getParentElement() == element.getOffsetParent(); + return parentIsOffsetParent ? top : top - element.getParentElement().getOffsetTop(); + } + +} \ No newline at end of file From 8f290d5f18de80a4cf2b8c4ec300a0c334596ddb Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 24 Jul 2018 18:01:57 +0200 Subject: [PATCH 05/58] bug4483 Scroll tabbed series to the top when navigating via progress bar --- .../regattacompetition/RegattaCompetition.java | 9 +++++++++ .../home/mobile/places/event/races/RacesActivity.java | 6 ++++++ .../gwt/home/mobile/places/event/races/RacesView.java | 4 ++++ .../home/mobile/places/event/races/RacesViewImpl.java | 10 ++++++++++ 4 files changed, 29 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java index 45a344513b3..1cbe1137bf2 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java @@ -1,5 +1,8 @@ package com.sap.sailing.gwt.home.mobile.partials.regattacompetition; +import java.util.HashMap; +import java.util.Map; + import com.google.gwt.core.client.GWT; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; @@ -8,6 +11,7 @@ import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.home.communication.event.RaceCompetitionFormatSeriesDTO; import com.sap.sailing.gwt.home.shared.partials.regattacompetition.RegattaCompetitionView; +import com.sap.sse.gwt.client.DOMUtils; public class RegattaCompetition extends Composite implements RegattaCompetitionView { @@ -17,6 +21,7 @@ public class RegattaCompetition extends Composite implements RegattaCompetitionV } @UiField FlowPanel regattaSeriesContainerUi; + private final Map seriesNameToUiComponent = new HashMap<>(); public RegattaCompetition() { RegattaCompetitionResources.INSTANCE.css().ensureInjected(); @@ -31,8 +36,12 @@ public class RegattaCompetition extends Composite implements RegattaCompetitionV @Override public RegattaCompetitionSeriesView addSeriesView(RaceCompetitionFormatSeriesDTO series) { RegattaCompetitionSeries seriesView = new RegattaCompetitionSeries(series); + seriesNameToUiComponent.put(series.getSeriesName(), seriesView); regattaSeriesContainerUi.add(seriesView); return seriesView; } + public void scrollToSeries(final String seriesName) { + DOMUtils.scrollToTop(seriesNameToUiComponent.get(seriesName).getElement()); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesActivity.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesActivity.java index bac62f55092..52165919c9a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesActivity.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesActivity.java @@ -1,6 +1,7 @@ package com.sap.sailing.gwt.home.mobile.places.event.races; import java.util.List; +import java.util.Optional; import com.sap.sailing.gwt.home.communication.eventview.EventViewDTO; import com.sap.sailing.gwt.home.desktop.places.event.regatta.racestab.RegattaRacesPlace; @@ -32,4 +33,9 @@ public class RacesActivity extends AbstractEventActivity impl initQuickfinder(view, true); return view; } + + @Override + public Optional getPreferredSeriesName() { + return getPlace().getPreferredSeriesName(); + } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesView.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesView.java index e457c2af812..eca0d41c55d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesView.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesView.java @@ -1,10 +1,14 @@ package com.sap.sailing.gwt.home.mobile.places.event.races; +import java.util.Optional; + import com.sap.sailing.gwt.home.mobile.places.event.EventViewBase; public interface RacesView extends EventViewBase { public interface Presenter extends EventViewBase.Presenter { + + Optional getPreferredSeriesName(); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java index 1aca7f4fea0..307f364b80b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java @@ -7,11 +7,13 @@ import java.util.Map; import java.util.Set; import com.google.gwt.core.client.GWT; +import com.google.gwt.core.client.Scheduler; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.gwt.home.communication.event.EventReferenceWithStateDTO; import com.sap.sailing.gwt.home.communication.event.GetCompetitionFormatRacesAction; +import com.sap.sailing.gwt.home.communication.event.RaceCompetitionFormatSeriesDTO; import com.sap.sailing.gwt.home.communication.event.SimpleCompetitorDTO; import com.sap.sailing.gwt.home.communication.eventview.RegattaMetadataDTO; import com.sap.sailing.gwt.home.communication.race.SimpleRaceMetadataDTO; @@ -27,6 +29,7 @@ import com.sap.sailing.gwt.home.shared.partials.filter.RacesByCompetitorTextBoxF import com.sap.sailing.gwt.home.shared.partials.placeholder.InfoPlaceholder; import com.sap.sailing.gwt.home.shared.partials.regattacompetition.RegattaCompetitionPresenter; import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.gwt.dispatch.shared.commands.ListResult; public class RacesViewImpl extends AbstractEventView implements RacesView { @@ -71,6 +74,13 @@ public class RacesViewImpl extends AbstractEventView implem protected String getRaceViewerURL(SimpleRaceMetadataDTO raceMetadata, String mode) { return currentPresenter.getRaceViewerURL(raceMetadata, mode); } + + @Override + public void setData(ListResult data) { + super.setData(data); + Scheduler.get().scheduleDeferred( + () -> currentPresenter.getPreferredSeriesName().ifPresent(regattaCompetitionUi::scrollToSeries)); + } } private class RacesViewImplFilterPresenter extends FilterPresenter { From 95da32f91e702dc2fdeddc94e66fc9f1051a002c Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 25 Jul 2018 13:57:55 +0200 Subject: [PATCH 06/58] bug4483 Always show series name on top of the progress bar except there's only the default series --- .../multiregattalist/MultiRegattaListSteps.java | 3 ++- .../multiregattalist/MultiRegattaListStepsBody.java | 12 +++++------- .../home/mobile/partials/eventsteps/EventSteps.gss | 3 ++- .../home/mobile/partials/eventsteps/EventSteps.java | 3 ++- .../mobile/partials/eventsteps/EventStepsPhase.java | 10 ++++++---- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListSteps.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListSteps.java index aba27539ffb..30d3ba92244 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListSteps.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListSteps.java @@ -48,10 +48,11 @@ public class MultiRegattaListSteps extends Composite { double parts = Math.pow(seriesProgress.getMaxRacesPerFleet(), 0.4); sumParts += parts; } + final boolean showSeriesName = regattaProgress.getSeries().size() > 1; for(RegattaProgressSeriesDTO seriesProgress : regattaProgress.getSeries()) { double parts = Math.pow(seriesProgress.getMaxRacesPerFleet(), 0.4); double percentage = 100.0 * parts / sumParts; - MultiRegattaListStepsBody stepsBody = new MultiRegattaListStepsBody(seriesProgress); + MultiRegattaListStepsBody stepsBody = new MultiRegattaListStepsBody(seriesProgress, showSeriesName); allMultiregattaSteps.add(stepsBody); stepsBody.getElement().getStyle().setWidth(percentage, Unit.PCT); stepsContainerUi.appendChild(stepsBody.getElement()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java index a2dc107749d..a94a09658ef 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/partials/multiregattalist/MultiRegattaListStepsBody.java @@ -1,7 +1,5 @@ package com.sap.sailing.gwt.home.desktop.partials.multiregattalist; -import static com.sap.sailing.domain.common.LeaderboardNameConstants.DEFAULT_SERIES_NAME; - import java.util.Map; import java.util.Map.Entry; @@ -42,10 +40,10 @@ public class MultiRegattaListStepsBody extends UIObject implements RequiresResiz private final String seriesName, seriesNameMedium, seriesNameShort; private int seriesNameLength, seriesNameMediumLength; - public MultiRegattaListStepsBody(RegattaProgressSeriesDTO seriesProgress) { + MultiRegattaListStepsBody(RegattaProgressSeriesDTO seriesProgress, boolean showName) { setElement(uiBinder.createAndBindUi(this)); textContainerUi.getStyle().setVisibility(Visibility.HIDDEN); - longNameDummyUi.setInnerText(seriesName = caculateSeriesName(seriesProgress)); + longNameDummyUi.setInnerText(seriesName = caculateSeriesName(seriesProgress, showName)); mediumNameDummyUi.setInnerText(seriesNameMedium = caculateSeriesNameMedium()); seriesNameShort = caculateSeriesNameShort(); if (seriesProgress.isCompleted()) { @@ -93,9 +91,9 @@ public class MultiRegattaListStepsBody extends UIObject implements RequiresResiz } } - private String caculateSeriesName(RegattaProgressSeriesDTO seriesProgress) { - if (seriesProgress.getName() == null || seriesProgress.getName().isEmpty() - || DEFAULT_SERIES_NAME.equals(seriesProgress.getName())) { + private String caculateSeriesName(RegattaProgressSeriesDTO seriesProgress, boolean showName) { + if (!showName || seriesProgress.getName() == null || seriesProgress.getName().isEmpty()) { + nameUi.getStyle().setDisplay(Display.NONE); return I18N.races(); } else { return seriesProgress.getName(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss index 7471b4468c8..e3911716fb0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.gss @@ -38,6 +38,7 @@ /* padding: .666666666666667em 0; */ display: inline; vertical-align: middle; + margin-right: 0.333333333333333rem; } .eventsteps_phases_phase_progressbar { height: 1.266666666666667em; @@ -61,7 +62,7 @@ img.eventsteps_phases_phase_progress_icon { vertical-align: middle; height: 1.133333333333333em; width: 1.133333333333333em; - margin: 0 0.333333333333333em; + margin-right: 0.333333333333333em; } .eventsteps_phases_phase_progress_text { font-size: 0.866666666666667em; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java index 0f62c0792f3..a13a5fd4120 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventSteps.java @@ -41,9 +41,10 @@ public class EventSteps extends Composite implements RefreshableWidget 1; for (RegattaProgressSeriesDTO seriesProgress : data.getProgress().getSeries()) { final PlaceNavigation navigation = racesNavigationFactory.apply(seriesProgress.getName()); - regattaProgessUi.addContent(new EventStepsPhase(seriesProgress, navigation)); + regattaProgessUi.addContent(new EventStepsPhase(seriesProgress, navigation, showSeriesName)); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java index 141d2815cfb..9728319df95 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/eventsteps/EventStepsPhase.java @@ -1,7 +1,5 @@ package com.sap.sailing.gwt.home.mobile.partials.eventsteps; -import static com.sap.sailing.domain.common.LeaderboardNameConstants.DEFAULT_SERIES_NAME; - import java.util.Map; import java.util.Map.Entry; @@ -35,10 +33,14 @@ public class EventStepsPhase extends Composite { @UiField DivElement progressUi; @UiField DivElement fleetsContainerUi; - public EventStepsPhase(RegattaProgressSeriesDTO seriesProgress, PlaceNavigation placeNavigation) { + EventStepsPhase(RegattaProgressSeriesDTO seriesProgress, PlaceNavigation placeNavigation, boolean showName) { initWidget(uiBinder.createAndBindUi(this)); LinkUtil.configureForAction(anchorUi, () -> placeNavigation.goToPlace()); - nameUi.setInnerText(DEFAULT_SERIES_NAME.equals(seriesProgress.getName()) ? I18N.races() : seriesProgress.getName()); + if (showName) { + nameUi.setInnerText(seriesProgress.getName()); + } else { + nameUi.removeFromParent(); + } if (seriesProgress.isCompleted()) { progressUi.setInnerText(I18N.racesCount(seriesProgress.getTotalRaceCount())); } else { From 9c5983084887f0131c3c090636d5c5963e761fca Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 25 Jul 2018 17:11:46 +0200 Subject: [PATCH 07/58] bug4483 Simplified util method which scrolls an element to the top --- .../RegattaCompetition.java | 2 +- .../places/event/races/RacesViewImpl.java | 5 ++- .../src/com/sap/sse/gwt/client/DOMUtils.java | 37 ++++++++++--------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java index 1cbe1137bf2..9c0c0e3be8e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/partials/regattacompetition/RegattaCompetition.java @@ -42,6 +42,6 @@ public class RegattaCompetition extends Composite implements RegattaCompetitionV } public void scrollToSeries(final String seriesName) { - DOMUtils.scrollToTop(seriesNameToUiComponent.get(seriesName).getElement()); + DOMUtils.scrollToTop(seriesNameToUiComponent.get(seriesName)); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java index 307f364b80b..89bc76e0473 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/places/event/races/RacesViewImpl.java @@ -5,6 +5,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.Scheduler; @@ -78,8 +79,8 @@ public class RacesViewImpl extends AbstractEventView implem @Override public void setData(ListResult data) { super.setData(data); - Scheduler.get().scheduleDeferred( - () -> currentPresenter.getPreferredSeriesName().ifPresent(regattaCompetitionUi::scrollToSeries)); + final Consumer scrollCommand = regattaCompetitionUi::scrollToSeries; + Scheduler.get().scheduleDeferred(() -> currentPresenter.getPreferredSeriesName().ifPresent(scrollCommand)); } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DOMUtils.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DOMUtils.java index 639d68e0816..0e0f8ef8183 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DOMUtils.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/DOMUtils.java @@ -1,6 +1,8 @@ package com.sap.sse.gwt.client; +import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; +import com.google.gwt.user.client.ui.Widget; /** * Utility class contained convenience methods regarding DOM operations. @@ -11,27 +13,28 @@ public final class DOMUtils { } /** - * Scrolls the top of the provided {@link Element element} to the top of the viewport. + * Scrolls the top of the provided {@link Widget widget} to the topmost possible position in the viewport. * - * @param element - * {@link Element} to scroll to the top + * @param widget + * {@link Widget} to scroll to the topmost possible position */ - public static void scrollToTop(final Element element) { - int top = getOffsetTopOf(element); - Element current = element.getParentElement(); - while (current != null) { - if (top != current.getScrollTop()) { - current.setScrollTop(top); - } - top += getOffsetTopOf(current) - current.getScrollTop(); - current = current.getParentElement(); - } + public static void scrollToTop(final Widget widget) { + getViewportElement().setScrollTop(widget.getAbsoluteTop()); } - private static int getOffsetTopOf(final Element element) { - final int top = element.getOffsetTop(); - final boolean parentIsOffsetParent = element.getParentElement() == element.getOffsetParent(); - return parentIsOffsetParent ? top : top - element.getParentElement().getOffsetTop(); + /** + * Scrolls the top of the provided {@link Element element} to the topmost possible position in the viewport. + * + * @param element + * {@link Element} to scroll to the topmost possible position + */ + public static void scrollToTop(final Element element) { + getViewportElement().setScrollTop(element.getAbsoluteTop()); + } + + private static Element getViewportElement() { + final Document doc = Document.get(); + return doc.isCSS1Compat() ? doc.getDocumentElement() : doc.getBody(); } } \ No newline at end of file From f84be9535f4658265ecbedca2979ac4a68f1e0df Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Wed, 25 Jul 2018 17:12:43 +0200 Subject: [PATCH 08/58] bug4483 Always reset viewport/scroll to top when navigation on mobile devices --- .../sap/sailing/gwt/home/mobile/app/MobileActivityMapper.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobileActivityMapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobileActivityMapper.java index 7a31ba34000..5bac0e34c98 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobileActivityMapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/mobile/app/MobileActivityMapper.java @@ -34,6 +34,7 @@ import com.sap.sailing.gwt.home.shared.places.user.confirmation.ConfirmationPlac import com.sap.sailing.gwt.home.shared.places.user.passwordreset.PasswordResetActivityProxy; import com.sap.sailing.gwt.home.shared.places.user.passwordreset.PasswordResetPlace; import com.sap.sailing.gwt.home.shared.places.user.profile.AbstractUserProfilePlace; +import com.sap.sse.gwt.client.DOMUtils; public class MobileActivityMapper implements ActivityMapper { private final MobileApplicationClientFactory clientFactory; @@ -52,6 +53,7 @@ public class MobileActivityMapper implements ActivityMapper { SwitchingEntryPoint.reloadApp(); return null; } + DOMUtils.scrollToTop(clientFactory.getRoot()); if (place instanceof ErrorPlace) { return new ErrorActivityProxy((ErrorPlace) place, clientFactory); } else if (place instanceof StartPlace) { From e5aab5dd89868645c9a4c58f0ee22bfbc22d1f1a Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Mon, 6 Aug 2018 12:58:39 +0200 Subject: [PATCH 09/58] bug4695 Refactored RaceBoardEntryPoint for a better overview on nested asynchronous service calls --- .../gwt/ui/raceboard/RaceBoardEntryPoint.java | 273 ++++++++++-------- 1 file changed, 149 insertions(+), 124 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 4b6cfbe7256..487d63cf647 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardEntryPoint.java @@ -2,6 +2,9 @@ package com.sap.sailing.gwt.ui.raceboard; import java.util.Collections; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Style.Unit; @@ -12,6 +15,7 @@ import com.google.gwt.user.client.ui.DockLayoutPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.RootLayoutPanel; import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.gwt.common.authentication.FixedSailingAuthentication; import com.sap.sailing.gwt.common.authentication.SAPSailingHeaderWithAuthentication; import com.sap.sailing.gwt.common.communication.routing.ProvidesLeaderboardRouting; @@ -26,13 +30,15 @@ import com.sap.sailing.gwt.ui.client.RemoteServiceMappingConstants; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.shared.RaceWithCompetitorsAndBoatsDTO; import com.sap.sailing.gwt.ui.shared.RaceboardDataDTO; +import com.sap.sailing.gwt.ui.shared.StrippedLeaderboardDTO; +import com.sap.sse.common.settings.Settings; import com.sap.sse.gwt.client.EntryPointHelper; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.formfactor.DeviceDetector; import com.sap.sse.gwt.client.player.Timer; import com.sap.sse.gwt.client.player.Timer.PlayModes; -import com.sap.sse.gwt.client.shared.components.Component; import com.sap.sse.gwt.client.shared.perspective.PerspectiveCompositeSettings; +import com.sap.sse.gwt.client.shared.settings.ComponentContext; import com.sap.sse.gwt.client.shared.settings.DefaultOnSettingsLoadedCallback; import com.sap.sse.gwt.settings.SettingsToUrlSerializer; import com.sap.sse.security.ui.authentication.generic.sapheader.SAPHeaderWithAuthentication; @@ -40,7 +46,6 @@ import com.sap.sse.security.ui.settings.ComponentContextWithSettingsStorage; import com.sap.sse.security.ui.settings.StoredSettingsLocation; public class RaceBoardEntryPoint extends AbstractSailingEntryPoint implements ProvidesLeaderboardRouting { - private RaceWithCompetitorsAndBoatsDTO selectedRace; /** * Controls the predefined mode into which to switch or configure the race viewer. @@ -48,131 +53,83 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint implements Pr private final MediaServiceAsync mediaService = GWT.create(MediaService.class); private RaceboardContextDefinition raceboardContextDefinition; - - private String leaderBoardName; @Override protected void doOnModuleLoad() { super.doOnModuleLoad(); EntryPointHelper.registerASyncService((ServiceDefTarget) mediaService, RemoteServiceMappingConstants.mediaServiceRemotePath); - raceboardContextDefinition = new SettingsToUrlSerializer() - .deserializeFromCurrentLocation(new RaceboardContextDefinition()); - final RaceBoardModes finalMode; - if (raceboardContextDefinition.getMode() != null) { - finalMode = RaceBoardModes.valueOf(raceboardContextDefinition.getMode()); - } else { - finalMode = null; - } - if (raceboardContextDefinition.getRegattaName() == null || raceboardContextDefinition.getRegattaName().isEmpty() - || raceboardContextDefinition.getRaceName() == null - || raceboardContextDefinition.getRaceName().isEmpty() - || raceboardContextDefinition.getLeaderboardName() == null - || raceboardContextDefinition.getLeaderboardName().isEmpty()) { - createErrorPage(getStringMessages().requiresRegattaRaceAndLeaderboard()); - return; - } - - leaderBoardName = raceboardContextDefinition.getLeaderboardName(); - - AsyncCallback asyncCallback = new AsyncCallback() { - @Override - public void onSuccess(RaceboardDataDTO raceboardData) { - if (!raceboardData.isValidLeaderboard()) { - createErrorPage(getStringMessages().noSuchLeaderboard()); - return; - } - if (raceboardContextDefinition.getEventId() != null && !raceboardData.isValidEvent()) { - createErrorPage(getStringMessages().noSuchEvent()); - } - if (raceboardContextDefinition.getLeaderboardGroupName() != null) { - if (!raceboardData.isValidLeaderboardGroup()) { - createErrorPage(getStringMessages().leaderboardNotContainedInLeaderboardGroup( - raceboardContextDefinition.getLeaderboardName(), - raceboardContextDefinition.getLeaderboardGroupName())); - return; - } - if (raceboardContextDefinition.getEventId() != null && raceboardData.isValidLeaderboardGroup() - && !raceboardData.isValidEvent()) { - createErrorPage(getStringMessages().leaderboardGroupNotContainedInEvent( - raceboardContextDefinition.getLeaderboardGroupName(), - raceboardContextDefinition.getEventId().toString())); - return; - } - } - if (raceboardData.getRace() == null) { - createErrorPage(getStringMessages().couldNotFindRaceInRegatta( - raceboardContextDefinition.getRaceName(), raceboardContextDefinition.getRegattaName())); - return; - } - final boolean showChartMarkEditMediaButtonsAndVideo = !DeviceDetector.isMobile(); - final StoredSettingsLocation storageDefinition = StoredSettingsLocationFactory - .createStoredSettingsLocatorForRaceBoard(raceboardContextDefinition, - finalMode != null ? finalMode.name() : null); - // TODO bug2822: also determine regatta's boat change parameter and parameterize Leaderboard settings - // --> show boat info column yes/no? - getSailingService().determineDetailTypesForCompetitorChart( - raceboardContextDefinition.getLeaderboardGroupName(), - raceboardData.getRace().getRaceIdentifier(), new AsyncCallback>() { - - @Override - public void onFailure(Throwable caught) { - reportError("Error trying to create the raceboard: " + caught.getMessage()); - } - - @Override - public void onSuccess(Iterable chartAllowedTypes) { - getSailingService().getAvailableDetailTypesForLeaderboard( - raceboardContextDefinition.getLeaderboardName(), - raceboardData.getRace().getRaceIdentifier(), new AsyncCallback>() { - @Override - public void onFailure(Throwable caught) { - reportError( - "Error trying to create the raceboard: " + caught.getMessage()); - } - - @Override - public void onSuccess(Iterable leaderboardAllowedTypes) { - final RaceBoardPerspectiveLifecycle lifeCycle = new RaceBoardPerspectiveLifecycle( - StringMessages.INSTANCE, chartAllowedTypes, getUserService(), - leaderboardAllowedTypes); - RaceBoardComponentContext componentContext = new RaceBoardComponentContext( - lifeCycle, getUserService(), storageDefinition); - - componentContext.getInitialSettings( - new DefaultOnSettingsLoadedCallback>() { - @Override - public void onSuccess( - PerspectiveCompositeSettings initialSettings) { - Timer timer = new Timer(PlayModes.Replay, 1000l); - final RaceBoardPanel raceBoardPanel = createPerspectivePage( - null, componentContext, initialSettings, - raceboardData, - showChartMarkEditMediaButtonsAndVideo, - lifeCycle, timer, leaderboardAllowedTypes); - if (finalMode != null) { - finalMode.getMode().applyTo(raceBoardPanel); - } - } - }); - } - }); - } + checkRaceboardContextPreConditions(new RaceboardContextDefinition()).ifPresent(raceboardContextDefinition -> { + RaceBoardEntryPoint.this.raceboardContextDefinition = raceboardContextDefinition; + final String modeString = raceboardContextDefinition.getMode(); + final RaceBoardModes mode = modeString == null ? null : RaceBoardModes.valueOf(modeString); + checkRaceboardDataConditions(raceboardData -> { + loadAvailableDetailTypes(raceboardData, (chartDetailTypes, lbDetailTypes) -> { + loadLeaderboardInformation(leaderboard -> { + initLifecycle(mode, chartDetailTypes, lbDetailTypes, leaderboard, (lifecycle, compContext) -> { + loadInitialSettings(compContext, settings -> { + createPerspective(mode, compContext, settings, raceboardData, lifecycle, lbDetailTypes); + }); }); - } + }); + }); + }); + }); + } + + private Optional checkRaceboardContextPreConditions(C settings) { + final C raceboardContext = new SettingsToUrlSerializer().deserializeFromCurrentLocation(settings); + if (raceboardContext.getRegattaName() == null || raceboardContext.getRegattaName().isEmpty() + || raceboardContext.getRaceName() == null || raceboardContext.getRaceName().isEmpty() + || raceboardContext.getLeaderboardName() == null || raceboardContext.getLeaderboardName().isEmpty()) { + createErrorPage(getStringMessages().requiresRegattaRaceAndLeaderboard()); + return Optional.empty(); + } + return Optional.of(raceboardContext); + } - @Override - public void onFailure(Throwable caught) { - reportError("Error trying to create the raceboard: " + caught.getMessage()); - } - }; + private void checkRaceboardDataConditions(final Consumer callback) { getSailingService().getRaceboardData(raceboardContextDefinition.getRegattaName(), raceboardContextDefinition.getRaceName(), raceboardContextDefinition.getLeaderboardName(), raceboardContextDefinition.getLeaderboardGroupName(), raceboardContextDefinition.getEventId(), - asyncCallback); + new AbstractRaceBoardAsyncCallback() { + + @Override + public void onSuccess(RaceboardDataDTO raceboardData) { + if (!raceboardData.isValidLeaderboard()) { + createErrorPage(getStringMessages().noSuchLeaderboard()); + return; + } + if (raceboardContextDefinition.getEventId() != null && !raceboardData.isValidEvent()) { + createErrorPage(getStringMessages().noSuchEvent()); + } + if (raceboardContextDefinition.getLeaderboardGroupName() != null) { + if (!raceboardData.isValidLeaderboardGroup()) { + createErrorPage(getStringMessages().leaderboardNotContainedInLeaderboardGroup( + raceboardContextDefinition.getLeaderboardName(), + raceboardContextDefinition.getLeaderboardGroupName())); + return; + } + if (raceboardContextDefinition.getEventId() != null + && raceboardData.isValidLeaderboardGroup() && !raceboardData.isValidEvent()) { + createErrorPage(getStringMessages().leaderboardGroupNotContainedInEvent( + raceboardContextDefinition.getLeaderboardGroupName(), + raceboardContextDefinition.getEventId().toString())); + return; + } + } + if (raceboardData.getRace() == null) { + createErrorPage(getStringMessages().couldNotFindRaceInRegatta( + raceboardContextDefinition.getRaceName(), + raceboardContextDefinition.getRegattaName())); + return; + } + callback.accept(raceboardData); + } + }); } - + private void createErrorPage(String message) { final DockLayoutPanel vp = new DockLayoutPanel(Unit.PX); final SAPHeaderWithAuthentication header = new SAPSailingHeaderWithAuthentication(); @@ -183,31 +140,99 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint implements Pr infoText.getElement().getStyle().setMargin(1, Unit.EM); vp.add(infoText); // TODO: Styling of error page slightly differs from the other usages of SAPSailingHeaderWithAuthentication - // because of the root font-size. Adjustments are postponed because they might affect the hole page content. + // because of the root font-size. Adjustments are postponed because they might affect the whole page content. } - private RaceBoardPanel createPerspectivePage(Component parent, + private void loadAvailableDetailTypes(final RaceboardDataDTO raceboardData, + final BiConsumer, Iterable> callback) { + final RegattaAndRaceIdentifier raceIdentifier = raceboardData.getRace().getRaceIdentifier(); + getSailingService().determineDetailTypesForCompetitorChart(raceboardContextDefinition.getLeaderboardGroupName(), + raceIdentifier, new AbstractRaceBoardAsyncCallback>() { + + @Override + public void onSuccess(Iterable allowedChartDetailTypes) { + final String leaderboardName = raceboardContextDefinition.getLeaderboardName(); + getSailingService().getAvailableDetailTypesForLeaderboard(leaderboardName, raceIdentifier, + new AbstractRaceBoardAsyncCallback>() { + + @Override + public void onSuccess(Iterable allowedLeaderboardDetailTypes) { + callback.accept(allowedChartDetailTypes, allowedLeaderboardDetailTypes); + } + }); + } + }); + } + + private void loadLeaderboardInformation(final Consumer callback) { + getSailingService().getLeaderboard(raceboardContextDefinition.getLeaderboardName(), + new AbstractRaceBoardAsyncCallback() { + + @Override + public void onSuccess(StrippedLeaderboardDTO result) { + callback.accept(result); + } + }); + } + + private void initLifecycle(final RaceBoardModes raceBoardMode, final Iterable chartDetailTypes, + final Iterable leaderboardDetailTypes, final StrippedLeaderboardDTO leaderboard, + final BiConsumer callback) { + final StoredSettingsLocation storageDefinition = StoredSettingsLocationFactory + .createStoredSettingsLocatorForRaceBoard(raceboardContextDefinition, + raceBoardMode != null ? raceBoardMode.name() : null); + final RaceBoardPerspectiveLifecycle lifeCycle = new RaceBoardPerspectiveLifecycle(leaderboard, + StringMessages.INSTANCE, chartDetailTypes, getUserService(), leaderboardDetailTypes); + RaceBoardComponentContext componentContext = new RaceBoardComponentContext(lifeCycle, getUserService(), + storageDefinition); + callback.accept(lifeCycle, componentContext); + } + + private void loadInitialSettings(final ComponentContext ctx, final Consumer callback) { + ctx.getInitialSettings(new DefaultOnSettingsLoadedCallback() { + + @Override + public void onSuccess(S settings) { + callback.accept(settings); + } + }); + } + + private void createPerspective(final RaceBoardModes raceBoardMode, ComponentContextWithSettingsStorage> context, PerspectiveCompositeSettings settings, RaceboardDataDTO raceboardData, - boolean showChartMarkEditMediaButtonsAndVideo, RaceBoardPerspectiveLifecycle raceLifeCycle, Timer timer, - Iterable availableDetailTypes) { - selectedRace = raceboardData.getRace(); + RaceBoardPerspectiveLifecycle raceLifeCycle, Iterable availableDetailTypes) { + final Timer timer = new Timer(PlayModes.Replay, 1000l); + final boolean showChartMarkEditMediaButtonsAndVideo = !DeviceDetector.isMobile(); + + final RaceWithCompetitorsAndBoatsDTO selectedRace = raceboardData.getRace(); Window.setTitle(selectedRace.getName()); AsyncActionsExecutor asyncActionsExecutor = new AsyncActionsExecutor(); - RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(getSailingService(), asyncActionsExecutor, this, - Collections.singletonList(selectedRace.getRaceIdentifier()), 5000l /* requestInterval*/); - RaceBoardPanel raceBoardPerspective = new RaceBoardPanel(parent, context, raceLifeCycle, settings, + RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(getSailingService(), + asyncActionsExecutor, this, Collections.singletonList(selectedRace.getRaceIdentifier()), + 5000l /* requestInterval */); + RaceBoardPanel raceBoardPerspective = new RaceBoardPanel(/* parent */ null, context, raceLifeCycle, settings, getSailingService(), mediaService, getUserService(), asyncActionsExecutor, raceboardData.getCompetitorAndTheirBoats(), timer, selectedRace.getRaceIdentifier(), raceboardContextDefinition.getLeaderboardName(), raceboardContextDefinition.getLeaderboardGroupName(), raceboardContextDefinition.getEventId(), RaceBoardEntryPoint.this, getStringMessages(), userAgent, raceTimesInfoProvider, showChartMarkEditMediaButtonsAndVideo, true, availableDetailTypes); RootLayoutPanel.get().add(raceBoardPerspective.getEntryWidget()); - return raceBoardPerspective; + + if (raceBoardMode != null) { + raceBoardMode.getMode().applyTo(raceBoardPerspective); + } } @Override public String getLeaderboardName() { - return leaderBoardName; + return raceboardContextDefinition.getLeaderboardName(); + } + + private abstract class AbstractRaceBoardAsyncCallback implements AsyncCallback { + @Override + public final void onFailure(Throwable caught) { + reportError("Error trying to create the raceboard: " + caught.getMessage()); + } } } \ No newline at end of file From 0fe9ed507e8d81f6c02358daab8965715803b374 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Mon, 6 Aug 2018 12:59:43 +0200 Subject: [PATCH 10/58] bug4695 Boat column default visibility now depends on canBoatsOfCompetitorsChangePerRace flag --- .../RaceBoardPerspectiveLifecycle.java | 10 ++------- .../SingleRaceLeaderboardPanelLifecycle.java | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPerspectiveLifecycle.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPerspectiveLifecycle.java index 7125da14d0b..f0f860d7cd0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPerspectiveLifecycle.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPerspectiveLifecycle.java @@ -28,13 +28,6 @@ public class RaceBoardPerspectiveLifecycle extends AbstractPerspectiveLifecycle< public static final String ID = "rb"; - //constructor used by Standalone RaceBoard - public RaceBoardPerspectiveLifecycle(StringMessages stringMessages, - Iterable competitorChartAllowedDetailTypes, UserService userService, - Iterable availableDetailTypes) { - this(null, stringMessages, competitorChartAllowedDetailTypes, userService, availableDetailTypes); - } - public RaceBoardPerspectiveLifecycle(AbstractLeaderboardDTO leaderboard, StringMessages stringMessages, Iterable competitorChartAllowedDetailTypes, UserService userService, Iterable availableDetailTypes) { @@ -42,7 +35,8 @@ public class RaceBoardPerspectiveLifecycle extends AbstractPerspectiveLifecycle< raceMapLifecycle = new RaceMapLifecycle(stringMessages); windChartLifecycle = new WindChartLifecycle(stringMessages); maneuverTableLifecycle = new ManeuverTableLifecycle(stringMessages); - leaderboardPanelLifecycle = new SingleRaceLeaderboardPanelLifecycle(stringMessages, availableDetailTypes); + leaderboardPanelLifecycle = new SingleRaceLeaderboardPanelLifecycle(stringMessages, availableDetailTypes, + leaderboard.canBoatsOfCompetitorsChangePerRace); multiCompetitorRaceChartLifecycle = new MultiCompetitorRaceChartLifecycle(stringMessages, competitorChartAllowedDetailTypes); mediaPlayerLifecycle = new MediaPlayerLifecycle(stringMessages); raceTimePanelLifecycle = new RaceTimePanelLifecycle(stringMessages, userService); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java index d75f0c8245f..e2bbbad0f13 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java @@ -16,10 +16,13 @@ public class SingleRaceLeaderboardPanelLifecycle extends LeaderboardPanelLifecyc private static final long DEFAULT_REFRESH_INTERVAL = 1000L; private final boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard; + private final boolean canBoatsOfCompetitorsChangePerRace; - public SingleRaceLeaderboardPanelLifecycle(StringMessages stringMessages, Iterable availableDetailTypes) { + public SingleRaceLeaderboardPanelLifecycle(final StringMessages stringMessages, + final Iterable availableDetailTypes, final boolean canBoatsOfCompetitorsChangePerRace) { super(stringMessages, availableDetailTypes); this.isScreenLargeEnoughToInitiallyDisplayLeaderboard = Document.get().getClientWidth() >= 1024; + this.canBoatsOfCompetitorsChangePerRace = canBoatsOfCompetitorsChangePerRace; } @Override @@ -33,25 +36,24 @@ public class SingleRaceLeaderboardPanelLifecycle extends LeaderboardPanelLifecyc List overallDetails = new ArrayList<>(); SingleRaceLeaderboardSettings defaultSettings = new SingleRaceLeaderboardSettings(); + // don't show competitor fullName column if even leaderboard isn't shown initially + final boolean showCompetitorFullNameColumn = isScreenLargeEnoughToInitiallyDisplayLeaderboard; + // the boat info is usually only interesting when boats of competitors can change per race + final boolean showCompetitorBoatInfoColumn = canBoatsOfCompetitorsChangePerRace; SingleRaceLeaderboardSettings settings = new SingleRaceLeaderboardSettings( defaultSettings.getManeuverDetailsToShow(), defaultSettings.getLegDetailsToShow(), defaultSettings.getRaceDetailsToShow(), overallDetails, DEFAULT_REFRESH_INTERVAL, defaultSettings.isShowAddedScores(), /* showCompetitorShortNameColumn */ true, - /* - * don't showCompetitorFullNameColumn in case screen is so small that we don't even display the - * leaderboard initially - */ isScreenLargeEnoughToInitiallyDisplayLeaderboard, - /* showCompetitorBoatInfoColumn */ true, false, false); + showCompetitorFullNameColumn, + showCompetitorBoatInfoColumn, + /* isCompetitorNationalityColumnVisible */ false, + /* showRaceRankColumn */ false); SettingsUtil.copyDefaultsFromValues(settings, settings); return settings; } - public boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard() { - return isScreenLargeEnoughToInitiallyDisplayLeaderboard; - } - @Override public SingleRaceLeaderboardSettings extractDocumentSettings(SingleRaceLeaderboardSettings currentLeaderboardSettings) { SingleRaceLeaderboardSettings defaultLeaderboardSettings = createDefaultSettings(); From 67a66453a97a67d84c53e89667bb0b696e8cbfd1 Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 7 Aug 2018 13:31:41 +0200 Subject: [PATCH 11/58] bug4695 Formatting and constructor visibility changes only --- .../client/leaderboard/LeaderboardPanelLifecycle.java | 2 +- .../leaderboard/LeaderboardSettingsDialogComponent.java | 3 ++- .../OverallLeaderboardSettingsDialogComponent.java | 6 ++++-- .../SingleRaceLeaderboardSettingsDialogComponent.java | 7 ++++--- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardPanelLifecycle.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardPanelLifecycle.java index f533c6d71e8..381b304b9c5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardPanelLifecycle.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardPanelLifecycle.java @@ -12,7 +12,7 @@ public abstract class LeaderboardPanelLifecycle protected final Iterable availableDetailTypes; - public LeaderboardPanelLifecycle(StringMessages stringMessages, Iterable availableDetailTypes) { + protected LeaderboardPanelLifecycle(StringMessages stringMessages, Iterable availableDetailTypes) { this.stringMessages = stringMessages; this.availableDetailTypes = availableDetailTypes; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsDialogComponent.java index 285bfba3f93..1b02fe8b583 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsDialogComponent.java @@ -46,7 +46,8 @@ public abstract class LeaderboardSettingsDialogComponent availableDetailTypes; - public LeaderboardSettingsDialogComponent(T initialSettings, StringMessages stringMessages, Iterable availableDetailTypes, boolean canBoatInfoBeShown) { + protected LeaderboardSettingsDialogComponent(T initialSettings, StringMessages stringMessages, + Iterable availableDetailTypes, boolean canBoatInfoBeShown) { this.initialSettings = initialSettings; this.stringMessages = stringMessages; this.canBoatInfoBeShown = canBoatInfoBeShown; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/OverallLeaderboardSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/OverallLeaderboardSettingsDialogComponent.java index a66666974f9..be79914e79e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/OverallLeaderboardSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/OverallLeaderboardSettingsDialogComponent.java @@ -9,8 +9,10 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog; public class OverallLeaderboardSettingsDialogComponent extends MultiRaceLeaderboardSettingsDialogComponent { - public OverallLeaderboardSettingsDialogComponent(MultiRaceLeaderboardSettings initialSettings, List allRaceColumnNames, StringMessages stringMessages, Iterable availableDetailTypes) { - super(initialSettings, allRaceColumnNames, stringMessages, availableDetailTypes, false); + public OverallLeaderboardSettingsDialogComponent(MultiRaceLeaderboardSettings initialSettings, + List allRaceColumnNames, StringMessages stringMessages, Iterable availableDetailTypes) { + super(initialSettings, allRaceColumnNames, stringMessages, availableDetailTypes, + /* canBoatInfoBeShown */ false); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettingsDialogComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettingsDialogComponent.java index 3f40d626b88..e911ec0cc11 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettingsDialogComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettingsDialogComponent.java @@ -13,11 +13,11 @@ import com.sap.sse.gwt.client.dialog.DataEntryDialog.Validator; public class SingleRaceLeaderboardSettingsDialogComponent extends LeaderboardSettingsDialogComponent { - protected CheckBox showRaceRankColumn; + private CheckBox showRaceRankColumn; public SingleRaceLeaderboardSettingsDialogComponent(SingleRaceLeaderboardSettings initialSettings, StringMessages stringMessages, Iterable availableDetailTypes) { - super(initialSettings, stringMessages, availableDetailTypes, true /*canBoatInfoBeShown*/); + super(initialSettings, stringMessages, availableDetailTypes, /* canBoatInfoBeShown */ true); } @Override @@ -33,7 +33,8 @@ public class SingleRaceLeaderboardSettingsDialogComponent 1000l * (delayBetweenAutoAdvancesValue == null ? 0l : delayBetweenAutoAdvancesValue.longValue()), /* showAddedScores */ showAddedScoresCheckBox.getValue().booleanValue(), showCompetitorShortNameColumnCheckBox.getValue(), showCompetitorFullNameColumnCheckBox.getValue(), - showCompetitorBoatInfoColumnCheckBox.getValue(), isCompetitorNationalityColumnVisible.getValue(), showRaceRankColumn.getValue()); + showCompetitorBoatInfoColumnCheckBox.getValue(), isCompetitorNationalityColumnVisible.getValue(), + showRaceRankColumn.getValue()); return newSettings; } From 1d55b53ec44834bfcfd427db7c4654a7b6cdd6da Mon Sep 17 00:00:00 2001 From: Benjamin Barth Date: Tue, 7 Aug 2018 14:31:50 +0200 Subject: [PATCH 12/58] bug4695 Disabled boat info columns in overall leaderboards (always) and multi-race leaderboard (when boat can change per race) --- .../leaderboard/MultiRaceLeaderboardPanelLifecycle.java | 5 ++++- .../gwt/ui/leaderboard/MultiRaceLeaderboardPanel.java | 8 +++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardPanelLifecycle.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardPanelLifecycle.java index 4e45d743d51..29a5b5a89e6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardPanelLifecycle.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardPanelLifecycle.java @@ -11,11 +11,13 @@ import com.sap.sse.common.settings.generic.support.SettingsUtil; public class MultiRaceLeaderboardPanelLifecycle extends LeaderboardPanelLifecycle { protected final List namesOfRaceColumns; + private final boolean canBoatInfoBeShown; public MultiRaceLeaderboardPanelLifecycle(AbstractLeaderboardDTO leaderboard, StringMessages stringMessages, Iterable availableDetailTypes) { super(stringMessages, availableDetailTypes); this.namesOfRaceColumns = leaderboard != null ? leaderboard.getNamesOfRaceColumns() : new ArrayList(); + this.canBoatInfoBeShown = leaderboard != null ? !leaderboard.canBoatsOfCompetitorsChangePerRace : false; } @Override public MultiRaceLeaderboardSettings extractUserSettings(MultiRaceLeaderboardSettings currentLeaderboardSettings) { @@ -55,7 +57,8 @@ public class MultiRaceLeaderboardPanelLifecycle extends LeaderboardPanelLifecycl @Override public MultiRaceLeaderboardSettingsDialogComponent getSettingsDialogComponent(MultiRaceLeaderboardSettings settings) { - return new MultiRaceLeaderboardSettingsDialogComponent(settings, namesOfRaceColumns, stringMessages, availableDetailTypes, false); + return new MultiRaceLeaderboardSettingsDialogComponent(settings, namesOfRaceColumns, stringMessages, + availableDetailTypes, canBoatInfoBeShown); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiRaceLeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiRaceLeaderboardPanel.java index 8c5dadc347a..42c94961eee 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiRaceLeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/MultiRaceLeaderboardPanel.java @@ -6,7 +6,6 @@ import java.util.List; import java.util.Map; import com.sap.sailing.domain.common.DetailType; -import com.sap.sailing.domain.common.LeaderboardType; import com.sap.sailing.domain.common.dto.AbstractLeaderboardDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.LeaderboardDTO; @@ -96,8 +95,7 @@ public class MultiRaceLeaderboardPanel extends LeaderboardPanel getSettingsDialogComponent( MultiRaceLeaderboardSettings useTheseSettings) { - return new MultiRaceLeaderboardSettingsDialogComponent((MultiRaceLeaderboardSettings) useTheseSettings, - leaderboard.getNamesOfRaceColumns(), stringMessages, availableDetailTypes, !leaderboard.canBoatsOfCompetitorsChangePerRace); + return new MultiRaceLeaderboardSettingsDialogComponent(useTheseSettings, leaderboard.getNamesOfRaceColumns(), + stringMessages, availableDetailTypes, canShowCompetitorBoatInfo()); } @Override From c435d082df6313c4a06b81ad5468470aaa6d60bd Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 9 Aug 2018 08:49:44 +0200 Subject: [PATCH 13/58] bug4713: added a failing test case Change-Id: I476037f021ee2a84170b5dd90b6bf8cab11157ae --- .../test/RaceStatusAnalyzerTest.java | 37 +++++++++++++++++-- .../race/state/impl/RaceStateImpl.java | 2 +- .../state/impl/ReadonlyRaceStateImpl.java | 20 ++-------- .../racingprocedure/RacingProcedure.java | 9 +++++ .../basic/impl/BasicRacingProcedureImpl.java | 2 +- .../ess/impl/ESSRacingProcedureImpl.java | 2 +- .../impl/GateStartRacingProcedureImpl.java | 2 +- .../impl/BaseRacingProcedure.java | 2 - .../impl/LeagueRacingProcedureImpl.java | 2 +- ...rableStartModeFlagRacingProcedureImpl.java | 3 -- .../line/impl/RRS26RacingProcedureImpl.java | 2 +- .../line/impl/SWCRacingProcedureImpl.java | 2 +- 12 files changed, 53 insertions(+), 32 deletions(-) diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java index 288fa7f2f0f..a8ea8066150 100644 --- a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Arrays; +import java.util.Collections; import org.junit.Test; import org.mockito.invocation.InvocationOnMock; @@ -18,11 +19,20 @@ 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.RaceLogRaceStatusEvent; +import com.sap.sailing.domain.abstractlog.race.RaceLogStartProcedureChangedEvent; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceStatusAnalyzer; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartProcedureChangedEventImpl; +import com.sap.sailing.domain.abstractlog.race.impl.SimpleRaceLogIdentifierImpl; +import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; +import com.sap.sailing.domain.abstractlog.race.state.impl.ReadonlyRaceStateImpl; import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.RacingProcedure; +import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.impl.ReadonlyRacingProcedureFactory; +import com.sap.sailing.domain.base.configuration.impl.EmptyRegattaConfiguration; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; +import com.sap.sailing.domain.common.racelog.RacingProcedureType; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsTimePoint; @@ -112,15 +122,36 @@ public class RaceStatusAnalyzerTest extends PassAwareRaceLogAnalyzerTestemptyMap(), /* update */ true); + Collections.emptyMap()); this.author = author; } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java index 4904be72af4..beb3f16c06b 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java @@ -187,7 +187,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL SimpleRaceLogIdentifier forRaceLogIdentifier, RacingProcedureFactory procedureFactory, Map dependentRaceStates) { this(raceLogResolver, raceLog, forRaceLogIdentifier, new RaceStatusAnalyzer.StandardClock(), - procedureFactory, dependentRaceStates, /* update */true); + procedureFactory, dependentRaceStates); } /** @@ -196,14 +196,11 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL * new race state. This can help avoid endless recursions because this race state can be passed along * together with the identifier so that when again along a dependent call chain a race state is required * for that race log identifier, this one can be used. - * @param update - * if true, the race state will be updated whenever the underlying raceLog - * changes. */ protected ReadonlyRaceStateImpl(RaceLogResolver raceLogResolver, RaceLog raceLog, SimpleRaceLogIdentifier forRaceLogIdentifier, Clock analyzersClock, RacingProcedureFactory procedureFactory, - final Map dependentRaceStates, boolean update) { + final Map dependentRaceStates) { this.raceLog = raceLog; this.raceLogResolver = raceLogResolver; this.procedureFactory = procedureFactory; @@ -325,6 +322,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL @Override public RaceLogRaceStatus getStatus() { + // TODO bug4713: check clock and invalidate/recalculate if clock passed status invalidation time point return cachedRaceStatus; } @@ -463,13 +461,11 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL recreateRacingProcedure(dependentRaceStates); changedListeners.onRacingProcedureChanged(this); } - RaceLogRaceStatus status = statusAnalyzer.analyze(); if (!Util.equalsWithNull(cachedRaceStatus, status)) { cachedRaceStatus = status; changedListeners.onStatusChanged(this); } - int passId = raceLog.getCurrentPassId(); if (cachedPassId != passId) { cachedPassId = passId; @@ -478,38 +474,32 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL cachedRacingProcedureType = null; cachedRacingProcedureTypeNoFallback = null; } - StartTimeFinderResult startTimeFinderResult = startTimeAnalyzer.analyze(); if (!Util.equalsWithNull(cachedStartTimeFinderResult, startTimeFinderResult)) { cachedStartTimeFinderResult = startTimeFinderResult; changedListeners.onStartTimeChanged(this); } adjustObserverForRelativeStartTime(startTimeFinderResult, dependentRaceStates); - TimePoint finishingTime = finishingTimeAnalyzer.analyze(); if (!Util.equalsWithNull(cachedFinishingTime, finishingTime)) { cachedFinishingTime = finishingTime; changedListeners.onFinishingTimeChanged(this); } - TimePoint finishedTime = finishedTimeAnalyzer.analyze(); if (!Util.equalsWithNull(cachedFinishedTime, finishedTime)) { cachedFinishedTime = finishedTime; changedListeners.onFinishedTimeChanged(this); } - TimeRange protest = protestTimeAnalyzer.analyze(); if (!Util.equalsWithNull(cachedProtest, protest)) { cachedProtest = protest; changedListeners.onProtestTimeChanged(this); } - CompetitorResults positionedCompetitors = finishPositioningListAnalyzer.analyze(); if (!Util.equalsWithNull(cachedPositionedCompetitors, positionedCompetitors)) { cachedPositionedCompetitors = positionedCompetitors; changedListeners.onFinishingPositioningsChanged(this); } - CompetitorResults confirmedPositionedCompetitors = confirmedFinishPositioningListAnalyzer.analyze(); if (!Util.equalsWithNull(cachedConfirmedPositionedCompetitors, confirmedPositionedCompetitors)) { cachedConfirmedPositionedCompetitors = confirmedPositionedCompetitors; @@ -517,13 +507,11 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL changedListeners.onFinishingPositionsConfirmed(this); } } - CourseBase courseDesign = courseDesignerAnalyzer.analyze(); if (!Util.equalsWithNull(cachedCourseDesign, courseDesign)) { cachedCourseDesign = courseDesign; changedListeners.onCourseDesignChanged(this); } - Wind windFix = lastWindFixAnalyzer.analyze(); if (!Util.equalsWithNull(cachedWindFix, windFix)) { cachedWindFix = windFix; @@ -539,10 +527,8 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL racingProcedure = procedureFactory.createRacingProcedure(cachedRacingProcedureType, raceLog, raceLogResolver); racingProcedure.setStateEventScheduler(scheduler); addChangedListener(racingProcedure); - statusAnalyzer = new RaceStatusAnalyzer(raceLogResolver, raceLog, statusAnalyzerClock, racingProcedure); // let's do an update because status might have changed with new procedure update(dependentRaceStates); } - } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/RacingProcedure.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/RacingProcedure.java index 1eba9f3b8fc..dbc6d40861d 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/RacingProcedure.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/RacingProcedure.java @@ -1,5 +1,8 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure; +import java.util.Collection; + +import com.sap.sailing.domain.abstractlog.race.state.RaceStateEvent; import com.sap.sse.common.TimePoint; /** @@ -17,4 +20,10 @@ public interface RacingProcedure extends ReadonlyRacingProcedure { */ void removeIndividualRecall(TimePoint now); + /** + * Delivers the time points and types of the events around the start, each optionally + * leading to a change in race status. + */ + Collection createStartStateEvents(TimePoint startTime); + } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java index ffea26c734a..feaa5aeb464 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java @@ -66,7 +66,7 @@ public class BasicRacingProcedureImpl extends BaseRacingProcedure { } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Collection createStartStateEvents(TimePoint startTime) { return Arrays.asList(new RaceStateEventImpl(startTime, RaceStateEvents.START)); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java index ea009a6554f..0ae3f36f013 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java @@ -64,7 +64,7 @@ public class ESSRacingProcedureImpl extends BaseRacingProcedure implements ESSRa } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Collection createStartStateEvents(TimePoint startTime) { return Arrays. asList(new RaceStateEventImpl(startTime.minus(startPhaseAPDownInterval), RaceStateEvents.ESS_AP_DOWN), new RaceStateEventImpl(startTime.minus(startPhaseESSThreeUpInterval), RaceStateEvents.ESS_THREE_UP), new RaceStateEventImpl(startTime.minus(startPhaseESSTwoUpInterval), diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java index fddb5aad31a..6a6d89679f2 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java @@ -119,7 +119,7 @@ public class GateStartRacingProcedureImpl extends BaseRacingProcedure implements } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Collection createStartStateEvents(TimePoint startTime) { return Arrays. asList( new RaceStateEventImpl(startTime.minus(startPhaseClassOverGolfUpInterval), RaceStateEvents.GATE_CLASS_OVER_GOLF_UP), diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/impl/BaseRacingProcedure.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/impl/BaseRacingProcedure.java index 1bccfd7b31b..017d0065bf8 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/impl/BaseRacingProcedure.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/impl/BaseRacingProcedure.java @@ -276,8 +276,6 @@ public abstract class BaseRacingProcedure extends BaseRaceStateChangedListener i return configuration; } - protected abstract Collection createStartStateEvents(TimePoint startTime); - @Override public void onStartTimeChanged(ReadonlyRaceState state) { getChangedListeners().onActiveFlagsChanged(this); diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java index a7098a8d0e5..703c2244571 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java @@ -81,7 +81,7 @@ public class LeagueRacingProcedureImpl extends BaseRacingProcedure implements Le } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Collection createStartStateEvents(TimePoint startTime) { // Remark: We are reusing here CLASS_UP, STARTMODE_UP and STARTMODE_DOWN from the RRS26 procedure // The question is why there are no generic CLASS_UP, STARTMODE_UP and STARTMODE_DOWN events return Arrays. asList( diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/ConfigurableStartModeFlagRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/ConfigurableStartModeFlagRacingProcedureImpl.java index 1e85749df58..684fe233401 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/ConfigurableStartModeFlagRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/ConfigurableStartModeFlagRacingProcedureImpl.java @@ -28,17 +28,14 @@ public abstract class ConfigurableStartModeFlagRacingProcedureImpl extends BaseR public ConfigurableStartModeFlagRacingProcedureImpl(RaceLog raceLog, AbstractLogEventAuthor author, ConfigurableStartModeFlagRacingProcedureConfiguration configuration, RaceLogResolver raceLogResolver) { super(raceLog, author, configuration, raceLogResolver); - RacingProcedureTypeAnalyzer procedureAnalyzer = new RacingProcedureTypeAnalyzer(raceLog); if (configuration.getStartModeFlags() != null) { this.startModeFlagAnalyzer = new StartModeFlagFinder(procedureAnalyzer, raceLog, configuration.getStartModeFlags()); } else { this.startModeFlagAnalyzer = new StartModeFlagFinder(procedureAnalyzer, raceLog, getDefaultStartModeFlags()); } - this.cachedStartmodeFlag = getDefaultStartMode(); this.startmodeFlagHasBeenSet = false; - update(); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java index 198b00ecc63..a2780f3daad 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java @@ -64,7 +64,7 @@ public class RRS26RacingProcedureImpl extends ConfigurableStartModeFlagRacingPro } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Collection createStartStateEvents(TimePoint startTime) { return Arrays. asList( new RaceStateEventImpl(startTime.minus(startPhaseClassUpInterval), RaceStateEvents.RRS26_CLASS_UP), new RaceStateEventImpl(startTime.minus(startPhaseStartModeUpInterval), RaceStateEvents.RRS26_STARTMODE_UP), diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java index 2a72bcddb68..18000930a16 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java @@ -69,7 +69,7 @@ public class SWCRacingProcedureImpl extends ConfigurableStartModeFlagRacingProce } @Override - protected Collection createStartStateEvents(TimePoint startTime) { + public Collection createStartStateEvents(TimePoint startTime) { return Arrays. asList( new RaceStateEventImpl(startTime.minus(CLASS_AND_STARTMODE_UP_INTERVAL), RaceStateEvents.SWC_CLASS_AND_STARTMODE_UP), new RaceStateEventImpl(startTime.minus(FIVE_MINUTES_FLAG_UP_INTERVAL), RaceStateEvents.SWC_FIVE_UP), From b43879ab29ab336bf633246335aa0637c50ded21 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 9 Aug 2018 11:07:23 +0200 Subject: [PATCH 14/58] bug4713: ReadonlyRaceState now updates its status if a status change was possible between last cache calculation time point and the request time point Change-Id: Iba391e2894951e19097e2ea30cdba9c4b4bed815 --- .../test/PassAwareRaceLogAnalyzerTest.java | 2 - .../test/RaceStatusAnalyzerTest.java | 49 +++++++---- .../racelog/state/test/RaceStateTest.java | 27 +++--- .../analyzing/impl/RaceStatusAnalyzer.java | 42 +++++---- .../race/state/RaceStateEventScheduler.java | 4 +- .../state/impl/ReadonlyRaceStateImpl.java | 87 +++++++++++++++++-- .../racingprocedure/RacingProcedure.java | 9 -- .../ReadonlyRacingProcedure.java | 8 ++ .../basic/impl/BasicRacingProcedureImpl.java | 3 +- .../ess/impl/ESSRacingProcedureImpl.java | 3 +- .../impl/GateStartRacingProcedureImpl.java | 3 +- .../impl/LeagueRacingProcedureImpl.java | 3 +- .../line/impl/RRS26RacingProcedureImpl.java | 3 +- .../line/impl/SWCRacingProcedureImpl.java | 3 +- .../RaceStateEventSchedulerOnService.java | 4 +- 15 files changed, 169 insertions(+), 81 deletions(-) diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/PassAwareRaceLogAnalyzerTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/PassAwareRaceLogAnalyzerTest.java index b200b547f9d..74818b821de 100644 --- a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/PassAwareRaceLogAnalyzerTest.java +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/PassAwareRaceLogAnalyzerTest.java @@ -67,10 +67,8 @@ public abstract class PassAwareRaceLogAnalyzerTest { +public class RaceStatusAnalyzerTest extends PassAwareRaceLogAnalyzerTest> { private RacingProcedure racingProcedure = mock(RacingProcedure.class); + private TimePoint now = MillisecondsTimePoint.now(); + private Clock constantTimeClock = ()->now; @Override protected RaceStatusAnalyzer createAnalyzer(RaceLog raceLog) { - return new RaceStatusAnalyzer(mock(RaceLogResolver.class), raceLog, racingProcedure); + return new RaceStatusAnalyzer(mock(RaceLogResolver.class), raceLog, constantTimeClock, racingProcedure); } @Override @@ -50,7 +55,7 @@ public class RaceStatusAnalyzerTest extends PassAwareRaceLogAnalyzerTest(event.getNextStatus(), now)); } @Override @@ -59,14 +64,14 @@ public class RaceStatusAnalyzerTest extends PassAwareRaceLogAnalyzerTest(event.getNextStatus(), now)); } @Test public void testUnscheduldedForNone() { RaceLogEvent event1 = createEvent(RaceLogEvent.class, 1); raceLog.add(event1); - assertEquals(RaceLogRaceStatus.UNSCHEDULED, analyzer.analyze()); + assertEquals(RaceLogRaceStatus.UNSCHEDULED, analyzer.analyze().getA()); } @Test @@ -80,7 +85,7 @@ public class RaceStatusAnalyzerTest extends PassAwareRaceLogAnalyzerTest { diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java index a1d5062a69a..0881da8cd97 100644 --- a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java @@ -140,33 +140,33 @@ public class RaceStateTest { } @Test - public void testAdvancePass() { + public void testAdvancePass() throws InterruptedException { state.addChangedListener(listener); int oldPassId = raceLog.getCurrentPassId(); - + Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setAdvancePass(mock(TimePoint.class)); - assertEquals(oldPassId + 1, raceLog.getCurrentPassId()); verify(listener).onAdvancePass(state); + verify(listener).onStatusChanged(state); verifyNoMoreInteractions(listener); } @Test - public void testAbort() { + public void testAbort() throws InterruptedException { state.addChangedListener(listener); - + Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setAborted(mock(TimePoint.class), false, Flags.NONE); - + verify(listener).onStatusChanged(state); // TODO: change test when interface is complete verifyNoMoreInteractions(listener); } @Test - public void testGeneralRecall() { + public void testGeneralRecall() throws InterruptedException { state.addChangedListener(listener); - + Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setGeneralRecall(mock(TimePoint.class)); - + verify(listener).onStatusChanged(state); // TODO: change test when interface is complete verifyNoMoreInteractions(listener); } @@ -174,20 +174,21 @@ public class RaceStateTest { @Test public void testInvalidateAfterAdvancePass() throws InterruptedException { state.addChangedListener(listener); - + Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.forceNewStartTime(nowMock, new MillisecondsTimePoint(1)); Thread.sleep(100); + Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setFinishedTime(new MillisecondsTimePoint(10)); + Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setCourseDesign(nowMock, mock(CourseBase.class), CourseDesignerMode.ADMIN_CONSOLE); - + Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setAdvancePass(mock(TimePoint.class)); - assertNull(state.getStartTime()); assertEquals(RaceLogRaceStatus.UNSCHEDULED, state.getStatus()); verify(listener, times(1)).onAdvancePass(state); verify(listener, times(2)).onStartTimeChanged(state); verify(listener, times(2)).onFinishedTimeChanged(state); - verify(listener, times(3)).onStatusChanged(state); + verify(listener, times(5)).onStatusChanged(state); verify(listener, times(1)).onCourseDesignChanged(state); verifyNoMoreInteractions(listener); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java index 5c7410afa56..617fb2c4e69 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java @@ -10,11 +10,19 @@ import com.sap.sailing.domain.abstractlog.race.impl.RaceLogRaceStatusEventCompar import com.sap.sailing.domain.abstractlog.race.state.racingprocedure.ReadonlyRacingProcedure; import com.sap.sailing.domain.common.racelog.RaceLogRaceStatus; import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.util.impl.ArrayListNavigableSet; -public class RaceStatusAnalyzer extends RaceLogAnalyzer { - +/** + * Returns the status and the clock {@link TimePoint} at which it was calculated. That time point is obtained + * from the {@link Clock} passed to this analyzer at construction time. It is queried at the time the analysis + * takes place ({@link #performAnalysis()}. + * + * @author Axel Uhl (d043530) + * + */ +public class RaceStatusAnalyzer extends RaceLogAnalyzer> { public interface Clock { TimePoint now(); } @@ -24,26 +32,25 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer { public TimePoint now() { return MillisecondsTimePoint.now(); } - } - private final EventDispatcher eventDispatcher; - private RaceLogResolver resolver; + private final RaceLogResolver resolver; + private final Clock clock; + private final ReadonlyRacingProcedure racingProcedure; public RaceStatusAnalyzer(RaceLogResolver resolver, RaceLog raceLog, ReadonlyRacingProcedure racingProcedure) { - super(raceLog); - this.eventDispatcher = new EventDispatcher(new StandardClock(), racingProcedure); - this.resolver = resolver; + this(resolver, raceLog, new StandardClock(), racingProcedure); } public RaceStatusAnalyzer(RaceLogResolver resolver, RaceLog raceLog, Clock clock, ReadonlyRacingProcedure racingProcedure) { super(raceLog); - this.eventDispatcher = new EventDispatcher(clock, racingProcedure); this.resolver = resolver; + this.clock = clock; + this.racingProcedure = racingProcedure; } @Override - protected RaceLogRaceStatus performAnalysis() { + protected Pair performAnalysis() { ArrayListNavigableSet statusEvents = new ArrayListNavigableSet<>( RaceLogRaceStatusEventComparator.INSTANCE); for (RaceLogEvent event : getPassEvents()) { @@ -51,21 +58,25 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer { statusEvents.add((RaceLogRaceStatusEvent) event); } } + final TimePoint now = clock.now(); + final EventDispatcher eventDispatcher = new EventDispatcher(now, racingProcedure); + RaceLogRaceStatus result = RaceLogRaceStatus.UNSCHEDULED; for (RaceLogRaceStatusEvent event : statusEvents.descendingSet()) { event.accept(eventDispatcher); - return eventDispatcher.nextStatus; + result = eventDispatcher.nextStatus; + break; } - return RaceLogRaceStatus.UNSCHEDULED; + return new Pair<>(result, now); } private class EventDispatcher extends BaseRaceLogEventVisitor { - private final Clock clock; + private final TimePoint now; private final ReadonlyRacingProcedure racingProcedure; public RaceLogRaceStatus nextStatus; - public EventDispatcher(Clock clock, ReadonlyRacingProcedure racingProcedure) { - this.clock = clock; + public EventDispatcher(TimePoint now, ReadonlyRacingProcedure racingProcedure) { + this.now = now; this.racingProcedure = racingProcedure; this.nextStatus = RaceLogRaceStatus.UNKNOWN; } @@ -80,7 +91,6 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer { if (startTime == null) { nextStatus = RaceLogRaceStatus.PRESCHEDULED; } else { - TimePoint now = clock.now(); if (racingProcedure.isStartphaseActive(startTime, now)) { nextStatus = RaceLogRaceStatus.STARTPHASE; } else if (now.before(startTime)) { diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateEventScheduler.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateEventScheduler.java index 0129123d77b..3637f3cdb3d 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateEventScheduler.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/RaceStateEventScheduler.java @@ -1,12 +1,10 @@ package com.sap.sailing.domain.abstractlog.race.state; -import java.util.Collection; - import com.sap.sailing.domain.abstractlog.race.state.impl.RaceStateEvents; public interface RaceStateEventScheduler { - void scheduleStateEvents(Collection stateEvents); + void scheduleStateEvents(Iterable stateEvents); void unscheduleStateEvent(RaceStateEvents stateEventName); diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java index beb3f16c06b..fd74cd0fea6 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java @@ -2,6 +2,8 @@ package com.sap.sailing.domain.abstractlog.race.state.impl; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import com.sap.sailing.domain.abstractlog.race.CompetitorResults; @@ -44,6 +46,8 @@ import com.sap.sailing.domain.common.racelog.RacingProcedureType; import com.sap.sse.common.TimePoint; import com.sap.sse.common.TimeRange; import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.impl.TimeRangeImpl; import com.sap.sse.util.WeakIdentityHashMap; /** @@ -139,7 +143,8 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL private RaceStateEventScheduler scheduler; /** - * We need one for each change in {@link RacingProcedureType}. + * We need one for each change in {@link RacingProcedureType}. The analyzer uses the {@link #statusAnalyzerClock} + * as its time source. */ private RaceStatusAnalyzer statusAnalyzer; private final RacingProcedureTypeAnalyzer racingProcedureAnalyzer; @@ -168,6 +173,22 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL private RacingProcedureType cachedRacingProcedureTypeNoFallback; private RaceLogRaceStatus cachedRaceStatus; + + /** + * The time taken from {@link #statusAnalyzerClock} when the race status was last updated. Set to a valid + * value whenever {@link #cachedRaceStatus} is set. + */ + private TimePoint clockTimePointWhenCachedRaceStatusWasSet; + + /** + * Time points determined during {@link #updateCachedRaceStatusAndValidity(RaceLogRaceStatus)} at which + * the race status may change. If the time range between {@link #clockTimePointWhenCachedRaceStatusWasSet} and + * the current {@link #statusAnalyzerClock clock} time point spans any of these, the {@link #cachedRaceStatus} + * is to be considered invalid and needs to be re-calculated. Set to a valid but possibly empty iterable + * whenever {@link #cachedRaceStatus} is set. + */ + private Iterable timePointsWhenRaceStatusMayChange; + private int cachedPassId; private StartTimeFinderResult cachedStartTimeFinderResult; private TimePoint cachedFinishingTime; @@ -323,9 +344,30 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL @Override public RaceLogRaceStatus getStatus() { // TODO bug4713: check clock and invalidate/recalculate if clock passed status invalidation time point + final TimePoint timePointForAnalysis = statusAnalyzerClock.now(); + final TimeRange timeRangeSinceLastStatusUpdate = new TimeRangeImpl(clockTimePointWhenCachedRaceStatusWasSet, timePointForAnalysis); + if (overlapsTimePointForPotentialStatusChange(timeRangeSinceLastStatusUpdate)) { + analyzeAndUpdateCachedRaceStatus(); + } return cachedRaceStatus; } + /** + * Finds out whether any of the time points in {@link #timePointsWhenRaceStatusMayChange} falls into the time range + * provided. In this case we assume that a race status update may have happened between the time point when the + * {@link #cachedRaceStatus} was updated last and the current request time point, forming the time range provided + * as parameter here. + */ + private boolean overlapsTimePointForPotentialStatusChange(TimeRange timeRangeSinceLastStatusUpdate) { + for (final TimePoint e : timePointsWhenRaceStatusMayChange) { + if (timeRangeSinceLastStatusUpdate.includes(e)) { + return true; + } + } + return false; + } + + @Override public TimePoint getStartTime() { return cachedStartTimeFinderResult.getStartTime(); @@ -461,11 +503,6 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL recreateRacingProcedure(dependentRaceStates); changedListeners.onRacingProcedureChanged(this); } - RaceLogRaceStatus status = statusAnalyzer.analyze(); - if (!Util.equalsWithNull(cachedRaceStatus, status)) { - cachedRaceStatus = status; - changedListeners.onStatusChanged(this); - } int passId = raceLog.getCurrentPassId(); if (cachedPassId != passId) { cachedPassId = passId; @@ -480,6 +517,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL changedListeners.onStartTimeChanged(this); } adjustObserverForRelativeStartTime(startTimeFinderResult, dependentRaceStates); + analyzeAndUpdateCachedRaceStatus(); TimePoint finishingTime = finishingTimeAnalyzer.analyze(); if (!Util.equalsWithNull(cachedFinishingTime, finishingTime)) { cachedFinishingTime = finishingTime; @@ -519,6 +557,43 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL } } + /** + * This method has protected visibility (and not private) to allow instrumenting by test cases + */ + protected void analyzeAndUpdateCachedRaceStatus() { + Pair statusAndTimePoint = statusAnalyzer.analyze(); + if (!Util.equalsWithNull(cachedRaceStatus, statusAndTimePoint)) { + updateCachedRaceStatusAndValidity(statusAndTimePoint); + } + } + + /** + * Updates the {@link #cachedRaceStatus} and calculates and remembers its validity, based on + * {@link ReadonlyRacingProcedure#createStartStateEvents(TimePoint)} and which of those have already + * passed at the current clock time and which ones are still lying ahead. + */ + private void updateCachedRaceStatusAndValidity(Pair statusAndTimePoint) { + cachedRaceStatus = statusAndTimePoint.getA(); + clockTimePointWhenCachedRaceStatusWasSet = statusAndTimePoint.getB(); + if (getStartTimeFinderResult() == null || getStartTime() == null) { + timePointsWhenRaceStatusMayChange = Collections.emptyList(); + } else { + final TimePoint startTime = getStartTime(); + List newTimePointsWhenRaceStatusMayChange = new LinkedList<>(); + for (final RaceStateEvent e : racingProcedure.createStartStateEvents(startTime)) { + newTimePointsWhenRaceStatusMayChange.add(e.getTimePoint()); + } + if (getFinishingTime() != null) { + newTimePointsWhenRaceStatusMayChange.add(getFinishingTime()); + } + if (getFinishedTime() != null) { + newTimePointsWhenRaceStatusMayChange.add(getFinishedTime()); + } + timePointsWhenRaceStatusMayChange = newTimePointsWhenRaceStatusMayChange; + } + changedListeners.onStatusChanged(this); + } + private void recreateRacingProcedure(Map dependentRaceStates) { if (racingProcedure != null) { removeChangedListener(racingProcedure); diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/RacingProcedure.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/RacingProcedure.java index dbc6d40861d..1eba9f3b8fc 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/RacingProcedure.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/RacingProcedure.java @@ -1,8 +1,5 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure; -import java.util.Collection; - -import com.sap.sailing.domain.abstractlog.race.state.RaceStateEvent; import com.sap.sse.common.TimePoint; /** @@ -20,10 +17,4 @@ public interface RacingProcedure extends ReadonlyRacingProcedure { */ void removeIndividualRecall(TimePoint now); - /** - * Delivers the time points and types of the events around the start, each optionally - * leading to a change in race status. - */ - Collection createStartStateEvents(TimePoint startTime); - } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ReadonlyRacingProcedure.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ReadonlyRacingProcedure.java index 92613aa02d7..323f21ea714 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ReadonlyRacingProcedure.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ReadonlyRacingProcedure.java @@ -4,6 +4,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogAnalyzer; import com.sap.sailing.domain.abstractlog.race.state.RaceState; import com.sap.sailing.domain.abstractlog.race.state.RaceStateChangedListener; +import com.sap.sailing.domain.abstractlog.race.state.RaceStateEvent; import com.sap.sailing.domain.abstractlog.race.state.RaceStateEventProcessor; import com.sap.sailing.domain.abstractlog.race.state.RaceStateEventScheduler; import com.sap.sailing.domain.abstractlog.race.state.ReadonlyRaceState; @@ -122,6 +123,13 @@ public interface ReadonlyRacingProcedure extends RaceStateEventProcessor, RaceSt RacingProcedurePrerequisite checkPrerequisitesForStart(TimePoint now, TimePoint startTime, RacingProcedurePrerequisite.FulfillmentFunction function); + + /** + * Delivers the time points and types of the events around the start, each optionally + * leading to a change in race status. + */ + Iterable createStartStateEvents(TimePoint startTime); + /** * This method is called by the framework. You don't need to call it. */ diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java index feaa5aeb464..da6f0efee9d 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/basic/impl/BasicRacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.basic.impl; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; @@ -66,7 +65,7 @@ public class BasicRacingProcedureImpl extends BaseRacingProcedure { } @Override - public Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { return Arrays.asList(new RaceStateEventImpl(startTime, RaceStateEvents.START)); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java index 0ae3f36f013..7be1ec4160b 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/ess/impl/ESSRacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.ess.impl; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; @@ -64,7 +63,7 @@ public class ESSRacingProcedureImpl extends BaseRacingProcedure implements ESSRa } @Override - public Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { return Arrays. asList(new RaceStateEventImpl(startTime.minus(startPhaseAPDownInterval), RaceStateEvents.ESS_AP_DOWN), new RaceStateEventImpl(startTime.minus(startPhaseESSThreeUpInterval), RaceStateEvents.ESS_THREE_UP), new RaceStateEventImpl(startTime.minus(startPhaseESSTwoUpInterval), diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java index 6a6d89679f2..5daae5a52ec 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/gate/impl/GateStartRacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.gate.impl; import java.util.Arrays; -import java.util.Collection; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.race.RaceLog; @@ -119,7 +118,7 @@ public class GateStartRacingProcedureImpl extends BaseRacingProcedure implements } @Override - public Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { return Arrays. asList( new RaceStateEventImpl(startTime.minus(startPhaseClassOverGolfUpInterval), RaceStateEvents.GATE_CLASS_OVER_GOLF_UP), diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java index 703c2244571..7dab122f8fb 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/league/impl/LeagueRacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.league.impl; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; @@ -81,7 +80,7 @@ public class LeagueRacingProcedureImpl extends BaseRacingProcedure implements Le } @Override - public Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { // Remark: We are reusing here CLASS_UP, STARTMODE_UP and STARTMODE_DOWN from the RRS26 procedure // The question is why there are no generic CLASS_UP, STARTMODE_UP and STARTMODE_DOWN events return Arrays. asList( diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java index a2780f3daad..bb728fda311 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/RRS26RacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.line.impl; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; @@ -64,7 +63,7 @@ public class RRS26RacingProcedureImpl extends ConfigurableStartModeFlagRacingPro } @Override - public Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { return Arrays. asList( new RaceStateEventImpl(startTime.minus(startPhaseClassUpInterval), RaceStateEvents.RRS26_CLASS_UP), new RaceStateEventImpl(startTime.minus(startPhaseStartModeUpInterval), RaceStateEvents.RRS26_STARTMODE_UP), diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java index 18000930a16..57f5428d08b 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/racingprocedure/line/impl/SWCRacingProcedureImpl.java @@ -1,7 +1,6 @@ package com.sap.sailing.domain.abstractlog.race.state.racingprocedure.line.impl; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.List; @@ -69,7 +68,7 @@ public class SWCRacingProcedureImpl extends ConfigurableStartModeFlagRacingProce } @Override - public Collection createStartStateEvents(TimePoint startTime) { + public Iterable createStartStateEvents(TimePoint startTime) { return Arrays. asList( new RaceStateEventImpl(startTime.minus(CLASS_AND_STARTMODE_UP_INTERVAL), RaceStateEvents.SWC_CLASS_AND_STARTMODE_UP), new RaceStateEventImpl(startTime.minus(FIVE_MINUTES_FLAG_UP_INTERVAL), RaceStateEvents.SWC_FIVE_UP), diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/services/RaceStateEventSchedulerOnService.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/services/RaceStateEventSchedulerOnService.java index 29ac1d61ec8..0a22ca93ba4 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/services/RaceStateEventSchedulerOnService.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/services/RaceStateEventSchedulerOnService.java @@ -1,7 +1,5 @@ package com.sap.sailing.racecommittee.app.services; -import java.util.Collection; - import com.sap.sailing.domain.abstractlog.race.state.RaceStateEvent; import com.sap.sailing.domain.abstractlog.race.state.RaceStateEventScheduler; import com.sap.sailing.domain.abstractlog.race.state.impl.RaceStateEvents; @@ -18,7 +16,7 @@ public class RaceStateEventSchedulerOnService implements RaceStateEventScheduler } @Override - public void scheduleStateEvents(Collection stateEvents) { + public void scheduleStateEvents(Iterable stateEvents) { for (RaceStateEvent stateEvent : stateEvents) { service.setAlarm(race, stateEvent); } From 6aac0335df626b3dcf865a15e880b001bba5f549 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 9 Aug 2018 11:28:14 +0200 Subject: [PATCH 15/58] bug4713: avoid phony race status updates if only the meta-attributes for caching have changed Change-Id: If2263925f9e15684cebb5be4f692d6ca69cda459 --- .../domain/racelog/state/test/RaceStateTest.java | 12 +----------- .../race/state/impl/ReadonlyRaceStateImpl.java | 7 +++++-- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java index 0881da8cd97..357ae765a9e 100644 --- a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/state/test/RaceStateTest.java @@ -143,20 +143,16 @@ public class RaceStateTest { public void testAdvancePass() throws InterruptedException { state.addChangedListener(listener); int oldPassId = raceLog.getCurrentPassId(); - Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setAdvancePass(mock(TimePoint.class)); assertEquals(oldPassId + 1, raceLog.getCurrentPassId()); verify(listener).onAdvancePass(state); - verify(listener).onStatusChanged(state); verifyNoMoreInteractions(listener); } @Test public void testAbort() throws InterruptedException { state.addChangedListener(listener); - Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setAborted(mock(TimePoint.class), false, Flags.NONE); - verify(listener).onStatusChanged(state); // TODO: change test when interface is complete verifyNoMoreInteractions(listener); } @@ -164,9 +160,7 @@ public class RaceStateTest { @Test public void testGeneralRecall() throws InterruptedException { state.addChangedListener(listener); - Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setGeneralRecall(mock(TimePoint.class)); - verify(listener).onStatusChanged(state); // TODO: change test when interface is complete verifyNoMoreInteractions(listener); } @@ -174,21 +168,17 @@ public class RaceStateTest { @Test public void testInvalidateAfterAdvancePass() throws InterruptedException { state.addChangedListener(listener); - Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.forceNewStartTime(nowMock, new MillisecondsTimePoint(1)); Thread.sleep(100); - Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setFinishedTime(new MillisecondsTimePoint(10)); - Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setCourseDesign(nowMock, mock(CourseBase.class), CourseDesignerMode.ADMIN_CONSOLE); - Thread.sleep(1); // ensure that a status update is triggered due to the calculation time stamps between set-up and pass advance vary state.setAdvancePass(mock(TimePoint.class)); assertNull(state.getStartTime()); assertEquals(RaceLogRaceStatus.UNSCHEDULED, state.getStatus()); verify(listener, times(1)).onAdvancePass(state); verify(listener, times(2)).onStartTimeChanged(state); verify(listener, times(2)).onFinishedTimeChanged(state); - verify(listener, times(5)).onStatusChanged(state); + verify(listener, times(3)).onStatusChanged(state); verify(listener, times(1)).onCourseDesignChanged(state); verifyNoMoreInteractions(listener); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java index fd74cd0fea6..0479340334b 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java @@ -517,7 +517,6 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL changedListeners.onStartTimeChanged(this); } adjustObserverForRelativeStartTime(startTimeFinderResult, dependentRaceStates); - analyzeAndUpdateCachedRaceStatus(); TimePoint finishingTime = finishingTimeAnalyzer.analyze(); if (!Util.equalsWithNull(cachedFinishingTime, finishingTime)) { cachedFinishingTime = finishingTime; @@ -528,6 +527,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL cachedFinishedTime = finishedTime; changedListeners.onFinishedTimeChanged(this); } + analyzeAndUpdateCachedRaceStatus(); // do this after start and finishing times have been updated TimeRange protest = protestTimeAnalyzer.analyze(); if (!Util.equalsWithNull(cachedProtest, protest)) { cachedProtest = protest; @@ -573,6 +573,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL * passed at the current clock time and which ones are still lying ahead. */ private void updateCachedRaceStatusAndValidity(Pair statusAndTimePoint) { + final RaceLogRaceStatus oldRaceStatus = cachedRaceStatus; cachedRaceStatus = statusAndTimePoint.getA(); clockTimePointWhenCachedRaceStatusWasSet = statusAndTimePoint.getB(); if (getStartTimeFinderResult() == null || getStartTime() == null) { @@ -591,7 +592,9 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL } timePointsWhenRaceStatusMayChange = newTimePointsWhenRaceStatusMayChange; } - changedListeners.onStatusChanged(this); + if (!Util.equalsWithNull(oldRaceStatus, cachedRaceStatus)) { + changedListeners.onStatusChanged(this); + } } private void recreateRacingProcedure(Map dependentRaceStates) { From f3fbc463fabb9583aff1e2f02e357bf0f15c239d Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 9 Aug 2018 12:57:40 +0200 Subject: [PATCH 16/58] consider only race log events valid at request time point Change-Id: Ica33a29f0a3b653b8f0b803c1e7b37dd5f2b0acd --- .../race/analyzing/impl/RaceStatusAnalyzer.java | 9 ++++++--- .../race/state/impl/ReadonlyRaceStateImpl.java | 1 - 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java index 617fb2c4e69..5197e22dd25 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/analyzing/impl/RaceStatusAnalyzer.java @@ -62,9 +62,12 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer(result, now); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java index 0479340334b..c6e847315d4 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java @@ -343,7 +343,6 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL @Override public RaceLogRaceStatus getStatus() { - // TODO bug4713: check clock and invalidate/recalculate if clock passed status invalidation time point final TimePoint timePointForAnalysis = statusAnalyzerClock.now(); final TimeRange timeRangeSinceLastStatusUpdate = new TimeRangeImpl(clockTimePointWhenCachedRaceStatusWasSet, timePointForAnalysis); if (overlapsTimePointForPotentialStatusChange(timeRangeSinceLastStatusUpdate)) { From 648198cfc2d9b2d4dc9e8671a2b8c260a358727b Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Thu, 9 Aug 2018 13:20:38 +0200 Subject: [PATCH 17/58] Bug 4695: Code format only change --- .../client/leaderboard/SingleRaceLeaderboardSettings.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java index edf6c10639a..509a76681f9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java @@ -17,8 +17,9 @@ public class SingleRaceLeaderboardSettings extends LeaderboardSettings { public SingleRaceLeaderboardSettings(Collection maneuverDetailsToShow, Collection legDetailsToShow, Collection raceDetailsToShow, Collection overallDetailsToShow, Long delayBetweenAutoAdvancesInMilliseconds, - boolean showAddedScores, boolean showCompetitorShortNameColumn, - boolean showCompetitorFullNameColumn, boolean showCompetitorBoatInfoColumn, boolean isCompetitorNationalityColumnVisible,boolean showRaceRankColumn) { + boolean showAddedScores, boolean showCompetitorShortNameColumn, boolean showCompetitorFullNameColumn, + boolean showCompetitorBoatInfoColumn, boolean isCompetitorNationalityColumnVisible, + boolean showRaceRankColumn) { super(maneuverDetailsToShow, legDetailsToShow, raceDetailsToShow, overallDetailsToShow, delayBetweenAutoAdvancesInMilliseconds, showAddedScores, showCompetitorShortNameColumn, showCompetitorFullNameColumn, showCompetitorBoatInfoColumn, isCompetitorNationalityColumnVisible); From 67d12dd282e7b9e3d5aa25b6138073570a10620c Mon Sep 17 00:00:00 2001 From: Steffen Schaefer Date: Thu, 9 Aug 2018 14:10:59 +0200 Subject: [PATCH 18/58] Bug 4695: Ensures that SingleRaceLeaderboardPanelLifecycle.createDefaultSettings sets the correct default for showCompetitorBoatInfoColumn to make the serialization does not always explicitly saves showCompetitorBoatInfoColumn in case of a leaderboard having canBoatsOfCompetitorsChangePerRace == true --- .../settings/client/leaderboard/LeaderboardSettings.java | 3 ++- .../client/leaderboard/LeaderboardSettingsFactory.java | 2 +- .../client/leaderboard/MultiRaceLeaderboardSettings.java | 3 ++- .../client/leaderboard/SingleRaceLeaderboardSettings.java | 4 ++-- .../ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java | 6 ++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettings.java index 99ad8b70c2e..1409a1046d9 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettings.java @@ -66,7 +66,8 @@ public abstract class LeaderboardSettings extends AbstractGenericSerializableSet showCompetitorBoatInfoColumn = new BooleanSetting("showCompetitorBoatInfoColumn", this, false); } - public LeaderboardSettings() { + public LeaderboardSettings(boolean showCompetitorBoatInfoColumnDefault) { + showCompetitorBoatInfoColumn.setDefaultValue(showCompetitorBoatInfoColumnDefault); } /** diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsFactory.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsFactory.java index 1dd0fb242a0..4f4778b2336 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsFactory.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsFactory.java @@ -44,7 +44,7 @@ public class LeaderboardSettingsFactory { } public SingleRaceLeaderboardSettings createNewSettingsWithCustomRaceDetails(List raceDetailsToShow) { - SingleRaceLeaderboardSettings defaultSettings = new SingleRaceLeaderboardSettings(); + SingleRaceLeaderboardSettings defaultSettings = new SingleRaceLeaderboardSettings(true); return new SingleRaceLeaderboardSettings( defaultSettings.getManeuverDetailsToShow(), defaultSettings.getLegDetailsToShow(), diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardSettings.java index 74232f6c701..fc9714d71fd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/MultiRaceLeaderboardSettings.java @@ -28,7 +28,7 @@ public class MultiRaceLeaderboardSettings extends LeaderboardSettings { public MultiRaceLeaderboardSettings() { - super(); + super(false); } public MultiRaceLeaderboardSettings(Collection maneuverDetailsToShow, @@ -48,6 +48,7 @@ public class MultiRaceLeaderboardSettings extends LeaderboardSettings { } public MultiRaceLeaderboardSettings(Iterable namesOfRaceColumnsToShow) { + super(false); this.namesOfRaceColumnsToShow.setValues(namesOfRaceColumnsToShow); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java index 509a76681f9..5c85a5d8a37 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/SingleRaceLeaderboardSettings.java @@ -10,8 +10,8 @@ public class SingleRaceLeaderboardSettings extends LeaderboardSettings { private BooleanSetting showRaceRankColumn; - public SingleRaceLeaderboardSettings() { - super(); + public SingleRaceLeaderboardSettings(boolean showCompetitorBoatInfoColumnDefault) { + super(showCompetitorBoatInfoColumnDefault); } public SingleRaceLeaderboardSettings(Collection maneuverDetailsToShow, diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java index e2bbbad0f13..6e06ca8f3eb 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SingleRaceLeaderboardPanelLifecycle.java @@ -34,19 +34,17 @@ public class SingleRaceLeaderboardPanelLifecycle extends LeaderboardPanelLifecyc raceDetails.add(DetailType.NUMBER_OF_MANEUVERS); raceDetails.add(DetailType.RACE_DISPLAY_LEGS); List overallDetails = new ArrayList<>(); - SingleRaceLeaderboardSettings defaultSettings = new SingleRaceLeaderboardSettings(); + SingleRaceLeaderboardSettings defaultSettings = new SingleRaceLeaderboardSettings(canBoatsOfCompetitorsChangePerRace); // don't show competitor fullName column if even leaderboard isn't shown initially final boolean showCompetitorFullNameColumn = isScreenLargeEnoughToInitiallyDisplayLeaderboard; - // the boat info is usually only interesting when boats of competitors can change per race - final boolean showCompetitorBoatInfoColumn = canBoatsOfCompetitorsChangePerRace; SingleRaceLeaderboardSettings settings = new SingleRaceLeaderboardSettings( defaultSettings.getManeuverDetailsToShow(), defaultSettings.getLegDetailsToShow(), defaultSettings.getRaceDetailsToShow(), overallDetails, DEFAULT_REFRESH_INTERVAL, defaultSettings.isShowAddedScores(), /* showCompetitorShortNameColumn */ true, showCompetitorFullNameColumn, - showCompetitorBoatInfoColumn, + /* showCompetitorBoatInfoColumn */ canBoatsOfCompetitorsChangePerRace, /* isCompetitorNationalityColumnVisible */ false, /* showRaceRankColumn */ false); SettingsUtil.copyDefaultsFromValues(settings, settings); From 6dbbbb1834c533717759402d183833996b113fc5 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 9 Aug 2018 17:17:53 +0200 Subject: [PATCH 19/58] bug4713: implemented fix for comment 9; tests locally green Change-Id: I9466ad6d383bc2072f18cb5ca278eaeb73a48fe3 --- .../analyzing/test/RaceLogAnalyzerTest.java | 3 +- .../test/RaceStatusAnalyzerTest.java | 29 ++++++++++++------- .../analyzing/impl/RaceStatusAnalyzer.java | 24 +++++++++++++-- 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceLogAnalyzerTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceLogAnalyzerTest.java index a40cda33219..a390a8fcd96 100644 --- a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceLogAnalyzerTest.java +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceLogAnalyzerTest.java @@ -30,7 +30,8 @@ public abstract class RaceLogAnalyzerTest T createEvent(Class type, long milliseconds) { - return createEvent(/* priority */ 0, type, milliseconds); + final T result = createEvent(/* priority */ 0, type, milliseconds); + return result; } protected static T createEvent(int priority, Class type, long milliseconds) { diff --git a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java index 4262010bd36..b564f94ac9c 100644 --- a/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java +++ b/java/com.sap.sailing.domain.shared.android.test/src/com/sap/sailing/domain/racelog/analyzing/test/RaceStatusAnalyzerTest.java @@ -24,6 +24,7 @@ import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceStatusAnalyzer; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceStatusAnalyzer.Clock; +import com.sap.sailing.domain.abstractlog.race.impl.RaceLogRaceStatusEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartProcedureChangedEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.RaceLogStartTimeEventImpl; import com.sap.sailing.domain.abstractlog.race.impl.SimpleRaceLogIdentifierImpl; @@ -76,25 +77,33 @@ public class RaceStatusAnalyzerTest extends PassAwareRaceLogAnalyzerTest statusTypesToIgnoreIfEventNotValidYet = new HashSet<>(Arrays.asList( + new RaceLogRaceStatus[] { RaceLogRaceStatus.FINISHED, RaceLogRaceStatus.FINISHING })); @Override protected Pair performAnalysis() { ArrayListNavigableSet statusEvents = new ArrayListNavigableSet<>( @@ -60,10 +66,24 @@ public class RaceStatusAnalyzer extends RaceLogAnalyzer statusesToIgnore = new HashSet<>(); RaceLogRaceStatus result = RaceLogRaceStatus.UNSCHEDULED; for (RaceLogRaceStatusEvent event : statusEvents.descendingSet()) { - // consider race log status events that are valid already at the time point we use as "now" - if (!event.getLogicalTimePoint().after(now)) { + // We need to read across a not yet valid FINISHED event and record that other FINISHED + // events need to be ignored; if we find a not yet valid FINISHING event, ignore further FINISHING + // events; but dispatch all other events (particularly start-related events) to the dispatcher: + final boolean dispatch; + if (statusTypesToIgnoreIfEventNotValidYet.contains(event.getNextStatus())) { + if (event.getLogicalTimePoint().after(now)) { // event not valid yet + statusesToIgnore.add(event.getNextStatus()); // but ignore events of same status with lesser relevance + dispatch = false; + } else { + dispatch = true; + } + } else { + dispatch = true; + } + if (dispatch) { event.accept(eventDispatcher); result = eventDispatcher.nextStatus; break; From 72b4aca18c1198386c3ef26bc8a30f96009dc6d7 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 9 Aug 2018 18:12:26 +0200 Subject: [PATCH 20/58] formatting only Change-Id: I5d742bacad55125735f4d9be3739803e3b18d01c --- .../sailing/server/gateway/jaxrs/spi/MasterDataResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java index 793c1a068cd..0bd41e30623 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/spi/MasterDataResource.java @@ -91,7 +91,7 @@ public class MasterDataResource extends AbstractSailingServerResource { getService().getSensorFixStore(), exportWind, deviceConfigurations); final StreamingOutput streamingOutput; if (compress) { - streamingOutput= new CompressingStreamingOutput(masterData, competitorIds, startTime); + streamingOutput = new CompressingStreamingOutput(masterData, competitorIds, startTime); } else { streamingOutput = new NonCompressingStreamingOutput(masterData, competitorIds, startTime); } From 1be37adf0ed3b3571466ec2b706b4fb30271faba Mon Sep 17 00:00:00 2001 From: "service.tip.git" Date: Thu, 9 Aug 2018 16:17:01 +0000 Subject: [PATCH 21/58] [INTERNAL] Translation delivery: commit by SLS Change-Id: I07339b01f6bc9f4073bc49fadfa1047e3a568a1d --- .../stringmessages/Sailing_StringMessages_es.properties | 3 ++- .../stringmessages/Sailing_StringMessages_fr.properties | 1 + .../stringmessages/Sailing_StringMessages_ja.properties | 1 + .../stringmessages/Sailing_StringMessages_pt.properties | 3 ++- .../stringmessages/Sailing_StringMessages_ru.properties | 3 ++- .../stringmessages/Sailing_StringMessages_zh.properties | 1 + 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties index 038c4898bc2..22d67d92adb 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_es.properties @@ -188,7 +188,7 @@ AbsRelativeBearingToNextMarkAfterManeuver=Rumbo relativo absoluto a la siguiente RatioBetweenDistanceSailedWithAndWithoutManeuver=Ratio entre distancia navegada con y sin maniobra RatioBetweenDistanceSailedTowardMiddleAngleProjectionWithAndWithoutManeuver=Ratio entre distancia navegada hacia la proyección del ángulo medio con y sin maniobra DurationLostByManeuver=Tiempo perdido por maniobra -DurationLostByManeuverTowardMiddleAngleProjection=tiempo perdido por maniobra hacia la proyección del ángulo medio +DurationLostByManeuverTowardMiddleAngleProjection=Tiempo perdido por maniobra hacia la proyección del ángulo medio JibingCount=Recuento de trasluchadas TackingCount=Recuento de viradas AbsTwaAtMaxTurningRate=TWA absoluto en velocidad máxima de giro @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=Porcentaje de progreso de curva p PercentageOfMainCurveProgressUntilMaxTurningRate=Porcentaje de progreso de curva principal hasta velocidad máxima de giro AbsTwaAtManeuverMiddle=TWA absoluto en mitad de maniobra GpsSamplingRate=Velocidad de muestreo de GPS +RaceDuration=Duración de carrera diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties index 6d9e3781cb4..3c9baf274ae 100755 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_fr.properties @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=Pourcentage de progression de la PercentageOfMainCurveProgressUntilMaxTurningRate=Pourcentage de progression de la courbe principale jusqu''à la vitesse angulaire max. AbsTwaAtManeuverMiddle=TWA absolu au milieu de la manœuvre GpsSamplingRate=Fréquence d''échantillonage GPS +RaceDuration=Durée de la course diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties index 153c78f1b5f..af7b95d25a7 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ja.properties @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=最高速度までの主曲線進 PercentageOfMainCurveProgressUntilMaxTurningRate=回転率最大速度までの主曲線進行の比率 (%) AbsTwaAtManeuverMiddle=マニューバー中間での絶対 TWA GpsSamplingRate=GPS サンプリング率 +RaceDuration=レース持続時間 diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties index 48c61759887..46904285579 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_pt.properties @@ -188,7 +188,7 @@ AbsRelativeBearingToNextMarkAfterManeuver=Rumo relativo absoluto até a marca se RatioBetweenDistanceSailedWithAndWithoutManeuver=Proporção entre distância navegada com e sem manobra RatioBetweenDistanceSailedTowardMiddleAngleProjectionWithAndWithoutManeuver=Proporção entre distância navegada em direção à projeção do ângulo médio com e sem manobra DurationLostByManeuver=Duração perdida por manobra -DurationLostByManeuverTowardMiddleAngleProjection=duração perdida por manobra em direção à projeção do ângulo médio +DurationLostByManeuverTowardMiddleAngleProjection=Duração perdida por manobra em direção à projeção do ângulo médio JibingCount=Contagem de jaibe TackingCount=Contagem de cambada AbsTwaAtMaxTurningRate=TWA absoluto na taxa de viragem máxima @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=Porcentagem de progresso da curva PercentageOfMainCurveProgressUntilMaxTurningRate=Porcentagem de progresso da curva principal até velocidade de taxa de viragem máxima AbsTwaAtManeuverMiddle=TWA absoluto no meio da manobra GpsSamplingRate=Taxa de amostra de GPS +RaceDuration=Duração da corrida diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties index bc36275620f..01ef682a6e3 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_ru.properties @@ -188,7 +188,7 @@ AbsRelativeBearingToNextMarkAfterManeuver=Абс. относительное н RatioBetweenDistanceSailedWithAndWithoutManeuver=Соотношение пройденного расстояния с маневром и без RatioBetweenDistanceSailedTowardMiddleAngleProjectionWithAndWithoutManeuver=Соотношение пройденного расстояния на срединную угловую проекцию с маневром и без DurationLostByManeuver=Потеря длительности за счет маневра -DurationLostByManeuverTowardMiddleAngleProjection=потеря длительности за счет маневра на срединную угловую проекцию +DurationLostByManeuverTowardMiddleAngleProjection=Потеря длительности за счет маневра на срединную угловую проекцию JibingCount=Поворотов фордевинд TackingCount=Поворотов оверштаг AbsTwaAtMaxTurningRate=Абс. TWA при макс. скорости поворота @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=Процент прохожден PercentageOfMainCurveProgressUntilMaxTurningRate=Процент прохождения основной кривой до макс. скорости поворота AbsTwaAtManeuverMiddle=Абс. TWA в середине маневра GpsSamplingRate=Частота дискретизации GPS +RaceDuration=Длительность гонки diff --git a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties index 3c6a3559da7..4156966e834 100644 --- a/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties +++ b/java/com.sap.sailing.datamining/resources/stringmessages/Sailing_StringMessages_zh.properties @@ -199,3 +199,4 @@ PercentageOfMainCurveProgressUntilHighestSpeed=最高速度前主要曲线进度 PercentageOfMainCurveProgressUntilMaxTurningRate=最大回转率速度前主要曲线进度的百分比 AbsTwaAtManeuverMiddle=操作中间的绝对实际风向角 GpsSamplingRate=GPS 采样率 +RaceDuration=比赛轮次持续时间 From aa64815bb750ded80642e8a5e4e2f04e7202f209 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 9 Aug 2018 18:41:02 +0200 Subject: [PATCH 22/58] bug4719: start with an empty course for SwissTimingRaceTrackerImpl, no longer depends on early CCG message Change-Id: If7b06e3ba2248e9c5d0660c6ee763ee8ba493639 --- .../swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java index 8b960a35e78..6d5bdcf9076 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java @@ -155,6 +155,8 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl connectivityParams.getRaceID(), connectivityParams.getRaceName(), connectivityParams.getRaceDescription(), connectivityParams.getBoatClass()); this.domainFactory = domainFactory; this.raceID = connectivityParams.getRaceID(); + // start out with an empty course, so we don't depend on receiving the CCG message before the timeout + this.course = new CourseImpl(this.raceID, /* start with empty marks list */ Collections.emptyList()); this.raceName = connectivityParams.getRaceName(); this.startList = connectivityParams.getStartList(); this.startListFromManage2Sail = connectivityParams.getStartList() != null; From 2458a231cf4cabbe6ae166e18110b313240dc097 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 9 Aug 2018 22:15:16 +0200 Subject: [PATCH 23/58] bug4704: added caching also for another overload of ReadonlyRaceStateImpl.create Change-Id: I8e231f62d5d091c028d4dc0f1de42730384f3e56 --- .../race/state/impl/ReadonlyRaceStateImpl.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java index c6e847315d4..accf15e0227 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/state/impl/ReadonlyRaceStateImpl.java @@ -71,6 +71,13 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL * See also bug4704. */ public static ReadonlyRaceState getOrCreate(RaceLogResolver raceLogResolver, RaceLog raceLog) { + return getOrCreate(raceLogResolver, raceLog, /* forRaceLogIdentifier */null, + Collections. emptyMap()); + } + + public static ReadonlyRaceState getOrCreate(RaceLogResolver raceLogResolver, RaceLog raceLog, + SimpleRaceLogIdentifier forRaceLogIdentifier, + Map dependentRaceStates) { WeakIdentityHashMap raceStatesForRaceLog; ReadonlyRaceState result = null; synchronized (raceStateCache) { @@ -82,7 +89,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL result = raceStatesForRaceLog.get(raceLogResolver); } if (result == null) { - result = createInternal(raceLogResolver, raceLog); + result = createInternal(raceLogResolver, raceLog, forRaceLogIdentifier, dependentRaceStates); raceStatesForRaceLog.put(raceLogResolver, result); } } @@ -90,9 +97,10 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL } - private static final ReadonlyRaceState createInternal(RaceLogResolver raceLogResolver, RaceLog raceLog) { - return create(raceLogResolver, raceLog, /* forRaceLogIdentifier */null, - Collections. emptyMap()); + private static final ReadonlyRaceState createInternal(RaceLogResolver raceLogResolver, RaceLog raceLog, + SimpleRaceLogIdentifier forRaceLogIdentifier, + Map dependentRaceStates) { + return create(raceLogResolver, raceLog, forRaceLogIdentifier, dependentRaceStates); } /** @@ -473,7 +481,7 @@ public class ReadonlyRaceStateImpl implements ReadonlyRaceState, RaceLogChangedL } else { RaceLog resolvedRaceLog = raceLogResolver.resolve(dependentOnRaceIdentifier); if (resolvedRaceLog != null) { - raceStateToObserve = ReadonlyRaceStateImpl.create(raceLogResolver, resolvedRaceLog, + raceStateToObserve = ReadonlyRaceStateImpl.getOrCreate(raceLogResolver, resolvedRaceLog, dependentOnRaceIdentifier, dependentRaceStates); raceStateToObserve.addChangedListener(raceStateToObserveListener); } From 7443901277e1ab439c5ff3bb34ceef221235c025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Sch=C3=A4fer?= Date: Fri, 10 Aug 2018 09:00:49 +0200 Subject: [PATCH 24/58] Bug 4695: Ensures that showCompetitorBoatInfoColumn is set to the default value for mode patching --- .../settings/client/leaderboard/LeaderboardSettingsFactory.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/settings/client/leaderboard/LeaderboardSettingsFactory.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsFactory.java index 4f4778b2336..1fa2202485b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsFactory.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/settings/client/leaderboard/LeaderboardSettingsFactory.java @@ -44,7 +44,7 @@ public class LeaderboardSettingsFactory { } public SingleRaceLeaderboardSettings createNewSettingsWithCustomRaceDetails(List raceDetailsToShow) { - SingleRaceLeaderboardSettings defaultSettings = new SingleRaceLeaderboardSettings(true); + SingleRaceLeaderboardSettings defaultSettings = new SingleRaceLeaderboardSettings(false); return new SingleRaceLeaderboardSettings( defaultSettings.getManeuverDetailsToShow(), defaultSettings.getLegDetailsToShow(), From 2f18f08a312862be75e8a3a746049cb637e728de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kai=20B=C3=B6rnert?= Date: Fri, 10 Aug 2018 10:27:50 +0200 Subject: [PATCH 25/58] bug4695 show boat column in sixty inch live leaderboard, if the race has changing boats --- ...ithRacemapAndLeaderBoardPresenterImpl.java | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/places/screens/liveraceloop/racemapwithleaderboard/LiveRaceWithRacemapAndLeaderBoardPresenterImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/places/screens/liveraceloop/racemapwithleaderboard/LiveRaceWithRacemapAndLeaderBoardPresenterImpl.java index 3debae7a209..2177bfcbf95 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/places/screens/liveraceloop/racemapwithleaderboard/LiveRaceWithRacemapAndLeaderBoardPresenterImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/autoplay/client/places/screens/liveraceloop/racemapwithleaderboard/LiveRaceWithRacemapAndLeaderBoardPresenterImpl.java @@ -12,16 +12,20 @@ import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.sap.sailing.domain.common.DetailType; +import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.dto.CompetitorDTO; +import com.sap.sailing.domain.common.dto.LeaderboardDTO; import com.sap.sailing.domain.common.dto.LeaderboardRowDTO; +import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.gwt.autoplay.client.app.AnimationPanel; import com.sap.sailing.gwt.autoplay.client.app.AutoPlayClientFactory; import com.sap.sailing.gwt.autoplay.client.app.AutoPlayPresenterConfigured; import com.sap.sailing.gwt.autoplay.client.utils.AutoplayHelper; import com.sap.sailing.gwt.settings.client.leaderboard.SingleRaceLeaderboardSettings; import com.sap.sailing.gwt.ui.client.FlagImageResolverImpl; +import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceCompetitorSet; @@ -152,12 +156,7 @@ public class LiveRaceWithRacemapAndLeaderBoardPresenterImpl view.showErrorNoLive(this, panel, new IllegalStateException("No race is live")); return; } - final SingleRaceLeaderboardSettings leaderboardSettings = new SingleRaceLeaderboardSettings( - /* maneuverDetailsToShow */ null, /* legDetailsToShow */ null, /* raceDetailsToShow */ null, - /* overallDetailsToShow */ null, /* delayBetweenAutoAdvancesInMilliseconds */ null, - /* showAddedScores */ false, /* showCompetitorShortNameColumn */ true, - /* showCompetitorFullNameColumn */ false, /* isCompetitorNationalityColumnVisible */ false, - /* showCompetitorBoatInfoColumn */ false, /* showRaceRankColumn */ true); + final SingleRaceLeaderboardSettings leaderboardSettings = getLeaderboardSettings(false); timer = new com.sap.sse.gwt.client.player.Timer( // perform the first request as "live" but don't by default auto-play PlayModes.Live, PlayStates.Playing, @@ -167,12 +166,42 @@ public class LiveRaceWithRacemapAndLeaderBoardPresenterImpl getSlideCtx().getContextDefinition().getLeaderboardName(), errorReporter, StringMessages.INSTANCE, false, null, false, null, false, true, false, false, false, new SixtyInchLeaderboardStyle(true), FlagImageResolverImpl.get(), Arrays.asList(DetailType.values())); + + leaderboardPanel.addLeaderboardUpdateListener(new LeaderboardUpdateListener() { + + @Override + public void updatedLeaderboard(LeaderboardDTO leaderboard) { + // if this is a changing boat race, enable the boat column + if (leaderboardPanel.getLeaderboard() != null) { + LeaderboardDTO leaderboardDTO = leaderboardPanel.getLeaderboard(); + boolean boatsRequired = leaderboardDTO.canBoatsOfCompetitorsChangePerRace; + if (leaderboardPanel.getSettings().isShowCompetitorBoatInfoColumn() != boatsRequired) { + leaderboardPanel.updateSettings(getLeaderboardSettings(boatsRequired)); + } + } + } + + @Override + public void currentRaceSelected(RaceIdentifier raceIdentifier, RaceColumnDTO raceColumn) { + } + }); + getPlace().getRaceMap().setQuickRanksDTOProvider(new QuickRanksDTOFromLeaderboardDTOProvider( new RaceCompetitorSet(getPlace().getRaceMapSelectionProvider()), liveRace)); view.startingWith(this, panel, getPlace().getRaceMap(), leaderboardPanel); selectionTimer.schedule(SWITCH_COMPETITOR_DELAY + AnimationPanel.ANIMATION_DURATION + AnimationPanel.DELAY); } + private SingleRaceLeaderboardSettings getLeaderboardSettings(boolean showBoatColumn) { + final SingleRaceLeaderboardSettings leaderboardSettings = new SingleRaceLeaderboardSettings( + /* maneuverDetailsToShow */ null, /* legDetailsToShow */ null, /* raceDetailsToShow */ null, + /* overallDetailsToShow */ null, /* delayBetweenAutoAdvancesInMilliseconds */ null, + /* showAddedScores */ false, /* showCompetitorShortNameColumn */ true, + /* showCompetitorFullNameColumn */ false, showBoatColumn, + /* showCompetitorBoatInfoColumn */ false, /* showRaceRankColumn */ true); + return leaderboardSettings; + } + @Override public void onStop() { if (timer != null) { From 4ba602b3a3dd85cdc5ba13181ffbacdeb77a78bd Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Fri, 10 Aug 2018 11:08:22 +0200 Subject: [PATCH 26/58] bug4719: create race with empty course when start list has been received through connectivity params Change-Id: I2b7e4354829b731d5e750cd3caa8d7c0e108c150 --- .../domain/swisstimingadapter/SwissTimingFactory.java | 5 +++-- .../swisstimingadapter/impl/SwissTimingFactoryImpl.java | 5 +++-- .../impl/SwissTimingRaceTrackerImpl.java | 7 +++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java index 33ea647eaa3..2dbbd85bff9 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/SwissTimingFactory.java @@ -1,6 +1,7 @@ package com.sap.sailing.domain.swisstimingadapter; import java.io.IOException; +import java.net.URISyntaxException; import java.net.UnknownHostException; import java.text.ParseException; @@ -53,11 +54,11 @@ public interface SwissTimingFactory { SwissTimingRaceTracker createRaceTracker(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, SwissTimingTrackingConnectivityParameters connectivityParams) - throws InterruptedException, UnknownHostException, IOException, ParseException; + throws InterruptedException, UnknownHostException, IOException, ParseException, URISyntaxException; RaceTracker createRaceTracker(Regatta regatta, WindStore windStore, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, SwissTimingTrackingConnectivityParameters connectivityParams) - throws UnknownHostException, InterruptedException, IOException, ParseException; + throws UnknownHostException, InterruptedException, IOException, ParseException, URISyntaxException; Race createRace(String raceId, String raceName, String description, BoatClass boatClass); diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java index 70236e46404..278bab7895d 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingFactoryImpl.java @@ -1,6 +1,7 @@ package com.sap.sailing.domain.swisstimingadapter.impl; import java.io.IOException; +import java.net.URISyntaxException; import java.net.UnknownHostException; import java.text.ParseException; import java.util.HashMap; @@ -91,7 +92,7 @@ public class SwissTimingFactoryImpl implements SwissTimingFactory { public SwissTimingRaceTracker createRaceTracker(RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, WindStore windStore, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, SwissTimingTrackingConnectivityParameters connectivityParams) - throws InterruptedException, UnknownHostException, IOException, ParseException { + throws InterruptedException, UnknownHostException, IOException, ParseException, URISyntaxException { return new SwissTimingRaceTrackerImpl(raceLogStore, regattaLogStore, windStore, domainFactory, this, trackedRegattaRegistry, raceLogResolver, connectivityParams); } @@ -100,7 +101,7 @@ public class SwissTimingFactoryImpl implements SwissTimingFactory { public RaceTracker createRaceTracker(Regatta regatta, WindStore windStore, DomainFactory domainFactory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, SwissTimingTrackingConnectivityParameters connectivityParams) - throws UnknownHostException, InterruptedException, IOException, ParseException { + throws UnknownHostException, InterruptedException, IOException, ParseException, URISyntaxException { return new SwissTimingRaceTrackerImpl(regatta, windStore, domainFactory, this, trackedRegattaRegistry, raceLogStore, regattaLogStore, raceLogResolver, connectivityParams); } diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java index 6d5bdcf9076..dad734cb2a5 100644 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/SwissTimingRaceTrackerImpl.java @@ -128,7 +128,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl WindStore windStore, DomainFactory domainFactory, SwissTimingFactory factory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogResolver raceLogResolver, SwissTimingTrackingConnectivityParameters connectivityParams) - throws InterruptedException, UnknownHostException, IOException, ParseException { + throws InterruptedException, UnknownHostException, IOException, ParseException, URISyntaxException { this(/* regatta */ null, windStore, domainFactory, factory, trackedRegattaRegistry, raceLogStore, regattaLogStore, raceLogResolver, connectivityParams); } @@ -136,7 +136,7 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl SwissTimingFactory factory, TrackedRegattaRegistry trackedRegattaRegistry, RaceLogStore raceLogStore, RegattaLogStore regattaLogStore, RaceLogResolver raceLogResolver, SwissTimingTrackingConnectivityParameters connectivityParams) - throws InterruptedException, UnknownHostException, IOException, ParseException { + throws InterruptedException, UnknownHostException, IOException, ParseException, URISyntaxException { super(connectivityParams); this.raceLogResolver = raceLogResolver; this.tmdMessageQueue = new TMDMessageQueue(this); @@ -172,6 +172,9 @@ public class SwissTimingRaceTrackerImpl extends AbstractRaceTrackerImpl this.updateURL = connectivityParams.getUpdateURL(); this.updateUsername = connectivityParams.getUpdateUsername(); this.updatePassword = connectivityParams.getUpdatePassword(); + if (connectivityParams.getStartList() != null) { + createRaceDefinition(course); + } } @Override From fbc79d131ad89b2b9aa8797735dfbc13b9f95325 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Sch=C3=A4fer?= Date: Fri, 10 Aug 2018 14:49:40 +0200 Subject: [PATCH 27/58] Fixed dashboards startup due to split packages com.sap.sse.util being used. This regression was introduced with commit 3b516bcbc3af9e8cd5295c19b51c88f87d942d95 --- java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF | 6 +++--- 1 file changed, 3 insertions(+), 3 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 904e059fb74..84eb7093503 100644 --- a/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF @@ -26,10 +26,10 @@ Require-Bundle: com.sap.sailing.domain, com.googlecode.mgwt;bundle-version="2.0.0";resolution:=optional, com.sap.sse.gwt.adminconsole, org.apache.commons.math;bundle-version="2.1.0", - com.sap.sse.security.ui + com.sap.sse.security.ui, + com.sap.sse Bundle-ActivationPolicy: lazy -Import-Package: com.sap.sse.util, - javax.servlet;version="3.1.0", +Import-Package: javax.servlet;version="3.1.0", javax.servlet.descriptor;version="3.1.0", javax.servlet.http;version="3.1.0", org.apache.commons.io, From 90a0630863109b38dfd6536d58bdb3eec71917b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kai=20B=C3=B6rnert?= Date: Fri, 10 Aug 2018 15:40:41 +0200 Subject: [PATCH 28/58] bug4721 added non null default ErrorReporter to RaceMap and RaceTimeInfoProvider constructor calls --- .../gwt/client/RibDashboardEntryPoint.java | 2 +- .../gwt/client/dashboardpanel/DashboardPanel.java | 5 +++-- .../startanalysis/StartAnalysisWidget.java | 13 ++++++++----- .../startanalysis/card/StartAnalysisCard.java | 15 ++++++++++----- 4 files changed, 22 insertions(+), 13 deletions(-) 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 82aabb4338f..e1d65f16d70 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 @@ -21,7 +21,7 @@ public class RibDashboardEntryPoint extends AbstractEntryPoint { public void doOnModuleLoad() { applyMGWTSettings(); DashboardClientFactory dashboardClientFactory = new DashboardClientFactoryImpl(); - DashboardPanel rootPanel = new DashboardPanel(dashboardClientFactory); + DashboardPanel rootPanel = new DashboardPanel(dashboardClientFactory, this); RootLayoutPanel.get().add(rootPanel); WindBotDataRetriever windBotDataRetriever = new WindBotDataRetriever(dashboardClientFactory); windBotDataRetriever.addNumberOfWindBotsChangeListeners(rootPanel); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dashboardpanel/DashboardPanel.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dashboardpanel/DashboardPanel.java index 56c672e0951..bb4caa36a73 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dashboardpanel/DashboardPanel.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/dashboardpanel/DashboardPanel.java @@ -36,6 +36,7 @@ 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.sailing.gwt.ui.shared.EventDTO; +import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.media.ImageDTO; /** @@ -88,12 +89,12 @@ public class DashboardPanel extends Composite implements NumberOfWindBotsChangeL DashboardClientFactory dashboardClientFactory; private static final Logger logger = Logger.getLogger(DashboardPanel.class.getName()); - public DashboardPanel(DashboardClientFactory dashboardClientFactory) { + public DashboardPanel(DashboardClientFactory dashboardClientFactory, ErrorReporter errorReporter) { DashboardPanelResources.INSTANCE.style().ensureInjected(); this.dashboardClientFactory = dashboardClientFactory; leftWindBotWidget = new WindBotWidget(dashboardClientFactory); rightWindBotWidget = new WindBotWidget(dashboardClientFactory); - startanalysisComponent = new StartAnalysisWidget(this.dashboardClientFactory); + startanalysisComponent = new StartAnalysisWidget(this.dashboardClientFactory, errorReporter); startlineAdvantagesByWindComponent = new StartlineAdvantagesByWindWidget(this.dashboardClientFactory); startlineAdvantageByGeometryWidget = new StartlineAdvantageByGeometryWidget(this.dashboardClientFactory); stringConstants = StringMessages.INSTANCE; diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/StartAnalysisWidget.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/StartAnalysisWidget.java index 787fe700495..f5d3c783b83 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/StartAnalysisWidget.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/StartAnalysisWidget.java @@ -43,6 +43,7 @@ import com.sap.sailing.dashboards.gwt.shared.dto.StartAnalysisDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapResources; +import com.sap.sse.gwt.client.ErrorReporter; /** * The class contains an collection of {@link StartAnalysisCard}s that are displayed in horizontal aligned pages. It @@ -112,11 +113,12 @@ public class StartAnalysisWidget extends Composite implements HasWidgets, PollsL * */ private int numberOfStartAnalysisCards = 0; private boolean displaysCards; - private List starts; - private List pageChangeListener; + private final List starts; + private final List pageChangeListener; private BottomNotification bottomNotification; private CompetitorSelectionPopup competitorSelectionPopup; - private DashboardClientFactory dashboardClientFactory; + private final DashboardClientFactory dashboardClientFactory; + private final ErrorReporter errorReporter; private final String cookieKeyForSelectedCompetitorInLeaderboard; @@ -131,8 +133,9 @@ public class StartAnalysisWidget extends Composite implements HasWidgets, PollsL /** * Component that contains handles, displays and loads startanalysis cards. * */ - public StartAnalysisWidget(DashboardClientFactory dashboardClientFactory) { + public StartAnalysisWidget(DashboardClientFactory dashboardClientFactory, ErrorReporter errorReporter) { StartAnalysisWidgetResources.INSTANCE.gss().ensureInjected(); + this.errorReporter = errorReporter; dashboardWidgetHeaderAndNoDataMessage = new DashboardWidgetHeaderAndNoDataMessage(); selectedCompetitorWithSettingsButton = new SettingsButtonWithSelectionIndicationLabel(); selectedCompetitorWithSettingsButton.disable(); @@ -378,7 +381,7 @@ public class StartAnalysisWidget extends Composite implements HasWidgets, PollsL } final StartAnalysisCard startlineAnalysisCard = new StartAnalysisCard(numberOfStartAnalysisCards * SCROLL_OFFSET_STARTANALYSIS_CARDS + MARGIN_LEFT_STARTANALYSIS_CARD, numberOfStartAnalysisCards, - startAnalysisDTO, dashboardClientFactory.getSailingService(), raceMapResources); + startAnalysisDTO, dashboardClientFactory.getSailingService(), errorReporter, raceMapResources); 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/widgets/startanalysis/card/StartAnalysisCard.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/card/StartAnalysisCard.java index 497dc402680..6c2442ab7a4 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/card/StartAnalysisCard.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/card/StartAnalysisCard.java @@ -38,6 +38,7 @@ import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapResources; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapSettings; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapZoomSettings; import com.sap.sailing.gwt.ui.client.shared.racemap.RaceMapZoomSettings.ZoomTypes; +import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.async.AsyncActionsExecutor; import com.sap.sse.gwt.client.player.Timer.PlayModes; @@ -70,8 +71,9 @@ public class StartAnalysisCard extends Composite implements HasWidgets, StartAna private StartAnalysisDTO startAnalysisDTO; private RaceMap raceMap; - private SailingServiceAsync sailingServiceAsync; - private StringMessages stringMessages; + private final SailingServiceAsync sailingServiceAsync; + private final ErrorReporter errorReporter; + private final StringMessages stringMessages; private final RaceCompetitorSelectionModel competitorSelectionModel; private final double WIND_LINE_ADVANTAGE_DIV_WIDTH_IN_PT = 185; @@ -80,9 +82,11 @@ public class StartAnalysisCard extends Composite implements HasWidgets, StartAna private RaceMapResources raceMapResources; - public StartAnalysisCard(double leftCSSProperty, int cardId, StartAnalysisDTO startAnalysisDTO, SailingServiceAsync sailingServiceAsync, RaceMapResources raceMapResources) { + public StartAnalysisCard(double leftCSSProperty, int cardId, StartAnalysisDTO startAnalysisDTO, + SailingServiceAsync sailingServiceAsync, ErrorReporter errorReporter, RaceMapResources raceMapResources) { stringMessages = StringMessages.INSTANCE; this.sailingServiceAsync = sailingServiceAsync; + this.errorReporter = errorReporter; this.raceMapResources = raceMapResources; competitorSelectionModel = new RaceCompetitorSelectionModel(/* hasMultiSelection */true); competitorSelectionModel.setCompetitors(startAnalysisDTO.getCompetitorDTOsFromStartAnaylsisCompetitorDTOs(), raceMap); @@ -167,10 +171,11 @@ public class StartAnalysisCard extends Composite implements HasWidgets, StartAna RaceTimesInfoProvider raceTimesInfoProvider = new RaceTimesInfoProvider(sailingServiceAsync, - asyncActionsExecutor, null, Collections.singletonList(startAnalysisDTO.regattaAndRaceIdentifier), 5000l /* requestInterval */); + asyncActionsExecutor, errorReporter, + Collections.singletonList(startAnalysisDTO.regattaAndRaceIdentifier), 5000l /* requestInterval */); raceMap = new RaceMap(null, null, new RaceMapLifecycle(StringMessages.INSTANCE), raceMapSettings, sailingServiceAsync, - asyncActionsExecutor, null, timer, competitorSelectionModel, + asyncActionsExecutor, errorReporter, timer, competitorSelectionModel, new RaceCompetitorSet(competitorSelectionModel), StringMessages.INSTANCE, startAnalysisDTO.regattaAndRaceIdentifier, raceMapResources, /* showHeaderPanel */ true, new DefaultQuickRanksDTOProvider()); raceTimesInfoProvider.addRaceTimesInfoProviderListener(raceMap); From 59389244b5f5056817f4710a18f79037243febba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Sch=C3=A4fer?= Date: Fri, 10 Aug 2018 15:48:18 +0200 Subject: [PATCH 29/58] Fixed a bug that caused RibDashboards to not find the SailingService --- .../gwt/ui/client/SailingServiceHelper.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceHelper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceHelper.java index 261db38393a..c4400b452d4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceHelper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceHelper.java @@ -2,14 +2,13 @@ package com.sap.sailing.gwt.ui.client; import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.rpc.ServiceDefTarget; +import com.sap.sse.gwt.client.EntryPointHelper; import com.sap.sse.gwt.client.ServiceRoutingProvider; /** * Set of convenience factory methods that create a {@link SailingServiceAsync} instance. */ public abstract class SailingServiceHelper { - private final static String moduleBaseURL = GWT.getModuleBaseURL(); - private SailingServiceHelper() { } @@ -41,20 +40,20 @@ public abstract class SailingServiceHelper { * @param sameBundle {@code false} if using {@link SailingService} from a different OSGi bundle (e.g. dashboards), {@code true} otherwise */ public static SailingServiceAsync createSailingServiceInstance(boolean sameBundle, ServiceRoutingProvider routingProvider) { - SailingServiceAsync service = GWT.create(SailingService.class); - ServiceDefTarget serviceToRegister = (ServiceDefTarget) service; - StringBuilder baseURL = new StringBuilder(); - baseURL.append(moduleBaseURL.substring(0, moduleBaseURL.lastIndexOf('/', moduleBaseURL.length() - 2) + 1)); - if (!sameBundle) { - baseURL.append(RemoteServiceMappingConstants.WEB_CONTEXT_PATH).append("/"); - } - - baseURL.append(RemoteServiceMappingConstants.sailingServiceRemotePath); + final SailingServiceAsync service = GWT.create(SailingService.class); + final ServiceDefTarget serviceToRegister = (ServiceDefTarget) service; + final StringBuilder servicePath = new StringBuilder(RemoteServiceMappingConstants.sailingServiceRemotePath); if (routingProvider != null) { - baseURL.append(routingProvider.routingSuffixPath()); + servicePath.append(routingProvider.routingSuffixPath()); + } + + final String servicePathWithRoutingSuffix = servicePath.toString(); + if (sameBundle) { + EntryPointHelper.registerASyncService(serviceToRegister, servicePathWithRoutingSuffix); + } else { + EntryPointHelper.registerASyncService(serviceToRegister, RemoteServiceMappingConstants.WEB_CONTEXT_PATH, servicePathWithRoutingSuffix); } - serviceToRegister.setServiceEntryPoint(baseURL.toString()); return service; } } From bef23514c533f4fce8667064920bc351edf66a0b Mon Sep 17 00:00:00 2001 From: Alessandro Stoltenberg Date: Sun, 12 Aug 2018 10:57:52 +0200 Subject: [PATCH 30/58] Added 470 Men and 470 Women as aliases to the BoatClassMasterdata --- .../src/com/sap/sailing/domain/common/BoatClassMasterdata.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java index a5d8736439f..b20c3ac4b5a 100755 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java @@ -17,7 +17,7 @@ public enum BoatClassMasterdata { _49ER ("49er", true, 4.88, 1.93, BoatHullType.MONOHULL, true), _49ERFX ("49erFX", true, 4.88, 1.93, BoatHullType.MONOHULL, true, "49FX"), _420 ("420", true, 4.20, 1.65, BoatHullType.MONOHULL, true, "420er", "420M", "420W"), - _470 ("470", true, 4.70, 1.68, BoatHullType.MONOHULL, true, "470er", "470M", "470W", "470 - M", "470 - W"), + _470 ("470", true, 4.70, 1.68, BoatHullType.MONOHULL, true, "470er", "470M", "470W", "470 - M", "470 - W", "470 Men", "470 Women"), _5O5 ("5O5", true, 5.03, 1.88, BoatHullType.MONOHULL, true, "505", "5o5", "505er", "5o5er"), _5_5MR ("5.5mR", true, 9.50, 1.92, BoatHullType.MONOHULL, true, "5.5 Meter", "5.5 Metre", "5.5", "5.5M", "5.5-metre", "5.5 metre"), _6MR ("6mR", true, 11, 2.2, BoatHullType.MONOHULL, true, "6 Meter", "6 Metre", "6", "6M", "6-metre", "6 metre"), From 7983dac58159c03d8cc23df1765082737b7ff80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kai=20B=C3=B6rnert?= Date: Mon, 13 Aug 2018 18:00:48 +0200 Subject: [PATCH 31/58] bug4724 differentiate between a redraw and a actual data chaning refresh. Do not clear markers if only redrawing --- .../gwt/ui/client/shared/racemap/RaceMap.java | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 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 5e7d29b4d3e..f5978714450 100755 --- 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 @@ -885,6 +885,10 @@ public class RaceMap extends AbstractCompositeComponent impleme @Override public void timeChanged(final Date newTime, final Date oldTime) { + boolean isRedraw = oldTime == null; + if(!isRedraw){ + GWT.debugger(); + } if (newTime != null && isMapInitialized) { if (raceIdentifier != null) { RegattaAndRaceIdentifier race = raceIdentifier; @@ -894,7 +898,8 @@ public class RaceMap extends AbstractCompositeComponent impleme final com.sap.sse.common.Util.Triple, Map, Map> fromAndToAndOverlap = fixesAndTails.computeFromAndTo(newTime, competitorsToShow, settings.getEffectiveTailLengthInMilliseconds()); // Request map data update, possibly in two calls; see method details - callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers(fromAndToAndOverlap, race, newTime, transitionTimeInMillis, competitorsToShow); + callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers( + fromAndToAndOverlap, race, newTime, transitionTimeInMillis, competitorsToShow, isRedraw); // draw the wind into the map, get the combined wind List windSourceTypeNames = new ArrayList(); windSourceTypeNames.add(WindSourceType.EXPEDITION.name()); @@ -950,6 +955,7 @@ public class RaceMap extends AbstractCompositeComponent impleme public WindInfoForRaceDTO getLastCombinedWindTrackInfoDTO() { return lastCombinedWindTrackInfoDTO; } + /** * Requests updates for map data and, when received, updates the map structures accordingly. *

@@ -970,12 +976,16 @@ public class RaceMap extends AbstractCompositeComponent impleme * current tail length setting the new tail segment does not overlap with the old one, requiring a full load of the * entire tail data for that competitor. For these non-overlapping requests, this method creates a * {@link GetBoatPositionsAction} request loading the entire tail required, but not quick ranks, sidelines and mark - * positions. Updating the results of this call is done in {@link #updateBoatPositions(Date, long, Map, Iterable, Map, boolean)}. + * positions. Updating the results of this call is done in + * {@link #updateBoatPositions(Date, long, Map, Iterable, Map, boolean)}. *

+ * + * @param isRedraw */ private void callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers( final Triple, Map, Map> fromAndToAndOverlap, - RegattaAndRaceIdentifier race, final Date newTime, final long transitionTimeInMillis, final Iterable competitorsToShow) { + RegattaAndRaceIdentifier race, final Date newTime, final long transitionTimeInMillis, + final Iterable competitorsToShow, boolean isRedraw) { final Map fromTimesForQuickCall = new HashMap<>(); final Map toTimesForQuickCall = new HashMap<>(); final Map fromTimesForNonOverlappingTailsCall = new HashMap<>(); @@ -1009,7 +1019,8 @@ public class RaceMap extends AbstractCompositeComponent impleme (settings.isShowSimulationOverlay() ? simulationOverlay.getLegIdentifier() : null), raceCompetitorSet.getMd5OfIdsAsStringOfCompetitorParticipatingInRaceInAlphanumericOrderOfTheirID(), newTime, settings.isShowEstimatedDuration()), GET_RACE_MAP_DATA_CATEGORY, - getRaceMapDataCallback(newTime, transitionTimeInMillis, fromAndToAndOverlap.getC(), competitorsToShow, ++boatPositionRequestIDCounter)); + getRaceMapDataCallback(newTime, transitionTimeInMillis, fromAndToAndOverlap.getC(), competitorsToShow, + ++boatPositionRequestIDCounter, isRedraw)); // next, if necessary, do the full thing; the two calls have different action classes, so throttling should not drop one for the other if (!fromTimesForNonOverlappingTailsCall.keySet().isEmpty()) { asyncActionsExecutor.execute(new GetBoatPositionsAction(sailingService, race, fromTimesForNonOverlappingTailsCall, toTimesForNonOverlappingTailsCall, @@ -1037,7 +1048,7 @@ public class RaceMap extends AbstractCompositeComponent impleme final Date newTime, final long transitionTimeInMillis, final Map hasTailOverlapForCompetitor, - final Iterable competitorsToShow, final int requestID) { + final Iterable competitorsToShow, final int requestID, boolean isRedraw) { return new MarkedAsyncCallback<>(new AsyncCallback() { @Override public void onFailure(Throwable caught) { @@ -1066,10 +1077,14 @@ public class RaceMap extends AbstractCompositeComponent impleme // Do boat specific actions Map> boatData = raceMapDataDTO.boatPositions; updateBoatPositions(newTime, transitionTimeInMillis, hasTailOverlapForCompetitor, competitorsToShow, boatData, /* updateTailsOnly */ false); - if (douglasMarkers != null) { - removeAllMarkDouglasPeuckerpoints(); + + if (!isRedraw) { + // only remove markers if the time is actually changed + if (douglasMarkers != null) { + removeAllMarkDouglasPeuckerpoints(); + } + maneuverMarkersAndLossIndicators.clearAllManeuverMarkers(); } - maneuverMarkersAndLossIndicators.clearAllManeuverMarkers(); if (requiresCoordinateSystemUpdateWhenCoursePositionAndWindDirectionIsKnown) { updateCoordinateSystemFromSettings(); } @@ -2433,7 +2448,7 @@ public class RaceMap extends AbstractCompositeComponent impleme showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); } else { // adding a single competitor; may need to re-load data, so refresh: - timeChanged(timer.getTime(), null); + redraw(); } } else { // only change highlighting @@ -2446,7 +2461,7 @@ public class RaceMap extends AbstractCompositeComponent impleme // 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); + redraw(); } } // Now update tails for all competitors because selection change may also affect all unselected competitors @@ -2469,13 +2484,13 @@ public class RaceMap extends AbstractCompositeComponent impleme 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); + redraw(); } 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); + redraw(); } else { // otherwise remove only deselected competitor's boat images and tail BoatOverlay removedBoatOverlay = boatOverlays.remove(competitor); @@ -2755,12 +2770,12 @@ public class RaceMap extends AbstractCompositeComponent impleme @Override public void competitorsListChanged(Iterable competitors) { - timeChanged(timer.getTime(), null); + redraw(); } @Override public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { - timeChanged(timer.getTime(), null); + redraw(); } @Override From 374aa7d895cb6ed8248e6d0771e16c2f8e0a8062 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 13 Aug 2018 18:13:39 +0200 Subject: [PATCH 32/58] adding pictures that explain how DNS mapping for dedicated sub-domains work Change-Id: I0f4ae3004a5115412df6c60a55d3054c16dd4ded --- wiki/images/amazon/CopyingAlbDnsName.png | Bin 0 -> 18963 bytes wiki/images/amazon/DNS1.png | Bin 0 -> 42546 bytes wiki/images/amazon/DNS2.png | Bin 0 -> 23860 bytes wiki/images/amazon/DNS3.png | Bin 0 -> 49616 bytes wiki/images/amazon/DNS4.png | Bin 0 -> 52922 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100755 wiki/images/amazon/CopyingAlbDnsName.png create mode 100755 wiki/images/amazon/DNS1.png create mode 100755 wiki/images/amazon/DNS2.png create mode 100755 wiki/images/amazon/DNS3.png create mode 100755 wiki/images/amazon/DNS4.png diff --git a/wiki/images/amazon/CopyingAlbDnsName.png b/wiki/images/amazon/CopyingAlbDnsName.png new file mode 100755 index 0000000000000000000000000000000000000000..b89bc5626f6d076fb25f5e5f282b9ed86482efa8 GIT binary patch literal 18963 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 USG}WKt^fc407*qoM6N<$f;T-Exc~qF literal 0 HcmV?d00001 diff --git a/wiki/images/amazon/DNS1.png b/wiki/images/amazon/DNS1.png new file mode 100755 index 0000000000000000000000000000000000000000..b65e132cd64fb926ea2ccbe7392b387f4bc39a7a GIT binary patch literal 42546 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 literal 0 HcmV?d00001 diff --git a/wiki/images/amazon/DNS2.png b/wiki/images/amazon/DNS2.png new file mode 100755 index 0000000000000000000000000000000000000000..841c53e4290b4c88a5e3bfac898d72682472c46b GIT binary patch literal 23860 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 XvMl`VxjdkBehhkBPoqTL_R0SM$|?w% literal 0 HcmV?d00001 diff --git a/wiki/images/amazon/DNS3.png b/wiki/images/amazon/DNS3.png new file mode 100755 index 0000000000000000000000000000000000000000..aec99b63a8fea6a252aadde3057c2f3b95fd4722 GIT binary patch literal 49616 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

X+J3e{8mvK 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 Date: Mon, 13 Aug 2018 18:14:57 +0200 Subject: [PATCH 33/58] added use-staged-third-party-artifacts Maven profile to allow for test builds against internal Nexus repository Change-Id: If960d04146d3be0f6e27f555371a824eb25339f4 --- configuration/maven-settings.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/configuration/maven-settings.xml b/configuration/maven-settings.xml index 8206a71f912..537d51554cb 100644 --- a/configuration/maven-settings.xml +++ b/configuration/maven-settings.xml @@ -33,6 +33,22 @@ + + + use-staged-third-party-artifacts + + + staged-third-party-artifacts + http://nexus.wdf.sap.corp:8081/nexus/content/repositories/3rd-party.releases.manual-uploads.all-closed-stages/ + + + + + staged-third-party-artifacts + http://nexus.wdf.sap.corp:8081/nexus/content/repositories/3rd-party.releases.manual-uploads.all-closed-stages/ + + + From 9b68b2f20972c772a1f13491034e6d6a4663efae Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 13 Aug 2018 18:21:38 +0200 Subject: [PATCH 34/58] removed GWT.debugger() statement Change-Id: I8ae65a5c99bfd26bd788a75a6f772bdc268dd6c1 --- configuration/buildAndUpdateProduct.sh | 2 +- .../com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/configuration/buildAndUpdateProduct.sh b/configuration/buildAndUpdateProduct.sh index a8133831ce8..a0f677f9500 100755 --- a/configuration/buildAndUpdateProduct.sh +++ b/configuration/buildAndUpdateProduct.sh @@ -634,7 +634,7 @@ if [[ "$@" == "build" ]] || [[ "$@" == "all" ]]; then SDK_MANAGER="$ANDROID_HOME/tools/bin/sdkmanager.bat" fi - mobile_extra="-P -with-not-android-relevant -P with-mobile" + mobile_extra="-P -with-not-android-relevant -P with-mobile -P use-staged-third-party-artifacts -Dmaven.repo.local=C:/tmp/temp_maven_repo" if [ $testing -eq 0 ]; then echo "INFO: Skipping tests" 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 f5978714450..67b9f96c1c8 100755 --- 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 @@ -886,9 +886,6 @@ public class RaceMap extends AbstractCompositeComponent impleme @Override public void timeChanged(final Date newTime, final Date oldTime) { boolean isRedraw = oldTime == null; - if(!isRedraw){ - GWT.debugger(); - } if (newTime != null && isMapInitialized) { if (raceIdentifier != null) { RegattaAndRaceIdentifier race = raceIdentifier; From 78de60c1a30d1b8d33e12abb0820c11224fed3df Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 13 Aug 2018 18:22:57 +0200 Subject: [PATCH 35/58] removed unfilled param comment Change-Id: If25e2f1c691608b37c8e7c6f9865d932383d953a --- .../com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java | 2 -- 1 file changed, 2 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 67b9f96c1c8..36deb163bac 100755 --- 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 @@ -976,8 +976,6 @@ public class RaceMap extends AbstractCompositeComponent impleme * positions. Updating the results of this call is done in * {@link #updateBoatPositions(Date, long, Map, Iterable, Map, boolean)}. *

- * - * @param isRedraw */ private void callGetRaceMapDataForAllOverlappingAndTipsOfNonOverlappingAndGetBoatPositionsForAllOthers( final Triple, Map, Map> fromAndToAndOverlap, From a964ea81210c041e351f9cd16ecba081eb181434 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 13 Aug 2018 16:23:33 +0000 Subject: [PATCH 36/58] Updated amazon ec2 (markdown) --- wiki/info/landscape/amazon-ec2.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/wiki/info/landscape/amazon-ec2.md b/wiki/info/landscape/amazon-ec2.md index 1f2668dec92..2b5a4d78ee8 100644 --- a/wiki/info/landscape/amazon-ec2.md +++ b/wiki/info/landscape/amazon-ec2.md @@ -430,6 +430,12 @@ In a live event scenario, the SAP Sailing Analytics are largely bandwidth bound. To still get the usual logging and URL re-writing features, replicas need to run their local Apache server with a bit of configuration. Luckily, most of the grunt work is done for you automatically. You simply need to tell the replicas in their instance details to start replicating automatically, provide an `EVENT_ID` and set the `SERVER_NAME` variable properly. The Apache configuration on the replica will then automatically be adjusted such that the lower-case version of $SERVER_NAME.sapsailing.com will re-direct users to the event page for the event with ID $EVENT_ID. +Amazon puts up limits regarding to the maximum number of rules that an Application Load Balancer (ALB) may have. We use one such ALB as the DNS CNAME target for ``*.sapsailing.com`` (Sailing-eu-west-1-135628335.eu-west-1.elb.amazonaws.com). Adding rules to this ALB is especially convenient because no DNS / Route53 manipulation is necessary at all. New sub-domains can be mapped to target groups this way quite flexibly and quickly. + +However, as the number of sub-domains we use grows, we also approach the limit of 100 rules for this load balancer. In order to keep this flexibility in particular for event set-ups, we started introducing more ALBs in August 2018 that use dedicated Route 53 DNS CNAME records for sepcific sub-domains. This way, with the current AWS limits for load balancers (see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-limits.html) we will have up to 20 ALBs per region with 100 rules each, giving us 2000 rules per region which should suffice for the foreseeable future. + +The set-up process needs to distinguish now between only adding a rule to an ALB listener targeted by the ``*.sapsailing.com`` DNS entry, and adding a rule to an ALB listener targeted only by DNS rules for specific sub-domains. In the latter case, a DNS record set needs to be created, providing the CNAME of the ALB that maps the sub-domain to the target group. + Here are the steps to create a load balanced setup, assuming there is already an "Application" load balancer defined in the region(s) where you need them: - Add a master+replica target group for the master and its replicas that external users will be directed to, using HTTP port 80 as the protocol settings. Note: as this target group will also be used for the HTTPS listener, "SSL offloading" will take place here. The re-directing from HTTP to HTTPS that shall occur when the user hits the server with an HTTP request will happen in the central instance's Apache server if and only if the `X-Forwarded-Proto` is `http` (https://stackoverflow.com/questions/26620670/apache-httpx-forwarded-proto-in-htaccess-is-causing-redirect-loop-in-dev-envir explains how a. See also http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/x-forwarded-headers.html#x-forwarded-proto.) @@ -441,7 +447,22 @@ Here are the steps to create a load balanced setup, assuming there is already an - Add a rule to the HTTPS listener for the hostname ${SERVER_NAME}-master.sapsailing.com that forwards traffic to the master-only target group just created. - Add the master to the master-only target group. - For both target groups configure the health checks, choosing HTTP as the protocol, using the default "traffic port" and setting the path to /index.html. Lower the interval to 10s and the "Healthy threshold" to 2 to ensure that servers are quickly recognized after adding them to the ELB. With the default settings (30 seconds interval, healthy threshold 10) this would last up to 5 minutes. -- When using the Race Committee App (RCApp), make sure the app is configured to send its data to the ${SERVER_NAME}-master.sapsailing.com URL (otherwise, write requests may end up at replicas which then have to reverse-replicate these to the master which adds significant overhead). +- When using the Race Committee App (RCApp), make sure the app is configured to send its data to the ${SERVER_NAME}-master.sapsailing.com URL (otherwise, write requests may end up at replicas which then have to reverse-replicate these to the master which adds significant overhead). + +The steps to register such a sub-domain mapping also in Route53 in case you've chosen an ALB that is not the target of ``*.sapsailing.com`` work as follows: + +Start by creating a new record set: + + +Then enter the sub-domain name you'd like to map. Choose ``CNAME`` for the type, reduce the default TTL to 60s and paste the DNS name of the ALB you'd like to target: + + +The DNS name of your load balancer can be copied from the "Basic Configuration" section in the "Description" tab: + + +The insertion of the rule into the ALB that maps your sub-domain's name to the corresponding target group works as usual and as described above: + + It is important to understand that it wouldn't help to let all traffic run through our central Apache httpd server which usually acts as a reverse proxy with comprehensive URL rewriting rules and macros. This would make the Apache server the bandwidth bottleneck. Instead, the event traffic needs to go straight to the ELB. This is established by the *.sapsailing.com DNS entry pointing to the Application ELB which then applies its filter rules to dispatch to the URL-specific target groups. Other than adding the hostname filter rules in the ELB as described above, no interaction with the Route 53 DNS is generally needed. Neither is it necessary to manually modify any 001-events.conf Apache configuration file. From f48e9ca74280ceade3e4f8e2f42f3eb2980f3cf9 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Mon, 13 Aug 2018 20:14:36 +0200 Subject: [PATCH 37/58] fixed accidental commit effets in buildAndUpdateProduct.sh file related to a test with staged Nexus artifacts Change-Id: I9ea038281bb1f0d4929b7029daba4a4b436add4e --- configuration/buildAndUpdateProduct.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/configuration/buildAndUpdateProduct.sh b/configuration/buildAndUpdateProduct.sh index a0f677f9500..a4f1c035436 100755 --- a/configuration/buildAndUpdateProduct.sh +++ b/configuration/buildAndUpdateProduct.sh @@ -634,7 +634,10 @@ if [[ "$@" == "build" ]] || [[ "$@" == "all" ]]; then SDK_MANAGER="$ANDROID_HOME/tools/bin/sdkmanager.bat" fi - mobile_extra="-P -with-not-android-relevant -P with-mobile -P use-staged-third-party-artifacts -Dmaven.repo.local=C:/tmp/temp_maven_repo" + # Uncomment the following line for testing an artifact stages in the SAP-central Nexus system: + # mobile_extra="-P -with-not-android-relevant -P with-mobile -P use-staged-third-party-artifacts -Dmaven.repo.local=${TMP}/temp_maven_repo" + # Use the following line for regular builds with no staged Nexus artifacts: + mobile_extra="-P -with-not-android-relevant -P with-mobile" if [ $testing -eq 0 ]; then echo "INFO: Skipping tests" From 60df2d9165b41fbaa7db5cef7201b335d50f3833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Sch=C3=A4fer?= Date: Tue, 14 Aug 2018 09:38:47 +0200 Subject: [PATCH 38/58] Bug 4727: Using SailingService from dashboards.gwt bundle --- .../dashboards/gwt/client/DashboardClientFactoryImpl.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/client/DashboardClientFactoryImpl.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/DashboardClientFactoryImpl.java index 57e263299b0..7076e8407c0 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 @@ -21,7 +21,7 @@ public class DashboardClientFactoryImpl implements DashboardClientFactory{ public DashboardClientFactoryImpl() { dispatch = new DashboardDispatchSystemImpl(); - sailingService = SailingServiceHelper.createSailingServiceInstance(false); + sailingService = SailingServiceHelper.createSailingServiceInstance(true); dashboardFiveSecondsTimer = new Timer(PlayModes.Live); dashboardFiveSecondsTimer.setRefreshInterval(DASHBOARD_RERFRESH_INTERVAL); From 9c66f8cdce214c20cd676103aa9512c4d5855e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Sch=C3=A4fer?= Date: Tue, 14 Aug 2018 09:39:52 +0200 Subject: [PATCH 39/58] Bug 4727: Fixed path to gwt-user sources in dashboards.gwt project --- java/com.google.gwt.servlet/.classpath | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.google.gwt.servlet/.classpath b/java/com.google.gwt.servlet/.classpath index a070fc0c879..fa521b36030 100644 --- a/java/com.google.gwt.servlet/.classpath +++ b/java/com.google.gwt.servlet/.classpath @@ -1,6 +1,6 @@ - + From 56d7e3dfb4363effc4db180951d559c3c98fab05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Sch=C3=A4fer?= Date: Tue, 14 Aug 2018 09:43:08 +0200 Subject: [PATCH 40/58] Bug 4727: Fixed startupUrl for dashboards.gwt launch configs --- .../GWT Dashboards DevMode.launch | 4 ++-- java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch index 7310aa92bc6..8f2860a0d0a 100644 --- a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch +++ b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch @@ -7,7 +7,7 @@ - + @@ -40,7 +40,7 @@ - + diff --git a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch index c5fc2f809c2..943761a8f3e 100644 --- a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch +++ b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch @@ -8,7 +8,7 @@ - + @@ -41,7 +41,7 @@ - + From e5dcc4a9f5e93dcb4576cbfd99bd68e55ad2fa9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Sch=C3=A4fer?= Date: Tue, 14 Aug 2018 09:44:57 +0200 Subject: [PATCH 41/58] Bug 4727: Removed unused dependencies from dashboards.gwt bundle and consolidated classpath for the launch configs --- .../GWT Dashboards DevMode.launch | 86 +++++++++--------- .../GWT Dashboards SDM.launch | 88 ++++++++++--------- .../META-INF/MANIFEST.MF | 7 +- 3 files changed, 90 insertions(+), 91 deletions(-) diff --git a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch index 8f2860a0d0a..24ad905fc2b 100644 --- a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch +++ b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards DevMode.launch @@ -1,47 +1,49 @@ - - - - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + diff --git a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch index 943761a8f3e..db0d10b4903 100644 --- a/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch +++ b/java/com.sap.sailing.dashboards.gwt/GWT Dashboards SDM.launch @@ -1,48 +1,50 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + 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 84eb7093503..d508c6cedfb 100644 --- a/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.dashboards.gwt/META-INF/MANIFEST.MF @@ -17,12 +17,7 @@ Require-Bundle: com.sap.sailing.domain, com.sap.sailing.gwt.ui, org.moxieapps.gwt.highcharts;bundle-version="1.6.0";resolution:=optional, com.sap.sse.gwt, - com.sap.sailing.domain.igtimiadapter, - com.sap.sailing.expeditionconnector, - com.sap.sailing.server.gateway, - com.sap.sailing.declination, - org.json.simple;bundle-version="1.1.0", - com.sap.sailing.udpconnector, + com.sap.sailing.expeditionconnector.common, com.googlecode.mgwt;bundle-version="2.0.0";resolution:=optional, com.sap.sse.gwt.adminconsole, org.apache.commons.math;bundle-version="2.1.0", From d68a72b728bac385f1b051cf587112eb9696180a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Sch=C3=A4fer?= Date: Tue, 14 Aug 2018 10:41:14 +0200 Subject: [PATCH 42/58] Bug 4727: fixed boats for dashboards UI --- .../widgets/startanalysis/card/StartAnalysisCard.java | 8 +++++++- .../actions/startanalysis/StartAnalysisDTOFactory.java | 2 ++ .../gwt/shared/dto/StartAnalysisCompetitorDTO.java | 2 ++ .../gwt/ui/client/RaceCompetitorSelectionModel.java | 6 ++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/card/StartAnalysisCard.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/card/StartAnalysisCard.java index 6c2442ab7a4..961e25cec89 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/card/StartAnalysisCard.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/client/widgets/startanalysis/card/StartAnalysisCard.java @@ -4,6 +4,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; +import java.util.Map; +import java.util.stream.Collectors; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.Scheduler; @@ -23,6 +25,8 @@ import com.sap.sailing.dashboards.gwt.client.widgets.startanalysis.StartAnalysis import com.sap.sailing.dashboards.gwt.client.widgets.startanalysis.rankingtable.StartAnalysisStartRankTable; import com.sap.sailing.dashboards.gwt.shared.StartlineAdvantageType; import com.sap.sailing.dashboards.gwt.shared.dto.StartAnalysisDTO; +import com.sap.sailing.domain.common.dto.BoatDTO; +import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.racelog.RacingProcedureType; import com.sap.sailing.gwt.ui.client.RaceCompetitorSelectionModel; import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; @@ -88,7 +92,9 @@ public class StartAnalysisCard extends Composite implements HasWidgets, StartAna this.sailingServiceAsync = sailingServiceAsync; this.errorReporter = errorReporter; this.raceMapResources = raceMapResources; - competitorSelectionModel = new RaceCompetitorSelectionModel(/* hasMultiSelection */true); + final Map competitorsToBoats = startAnalysisDTO.startAnalysisCompetitorDTOs.stream() + .collect(Collectors.toMap(c -> c.competitorDTO, c -> c.boatDTO)); + competitorSelectionModel = new RaceCompetitorSelectionModel(/* hasMultiSelection */true, competitorsToBoats); competitorSelectionModel.setCompetitors(startAnalysisDTO.getCompetitorDTOsFromStartAnaylsisCompetitorDTOs(), raceMap); initWidget(uiBinder.createAndBindUi(this)); startanalysis_card.getElement().getStyle().setLeft(leftCSSProperty, Unit.PCT); diff --git a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisDTOFactory.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisDTOFactory.java index 3f330bec180..a5ce58cd6c2 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisDTOFactory.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/server/util/actions/startanalysis/StartAnalysisDTOFactory.java @@ -193,6 +193,8 @@ public final class StartAnalysisDTOFactory extends AbstractStartAnalysisCreation StartAnalysisCompetitorDTO startAnalysisCompetitorDTOsForRace = new StartAnalysisCompetitorDTO(); startAnalysisCompetitorDTOsForRace.competitorDTO = dashboardDispatchContext.getRacingEventService().getBaseDomainFactory().getCompetitorAndBoatStore() .convertToCompetitorDTO(competitor); + startAnalysisCompetitorDTOsForRace.boatDTO = dashboardDispatchContext.getRacingEventService().getBaseDomainFactory().getCompetitorAndBoatStore() + .convertToBoatDTO(trackedRace.getBoatOfCompetitor(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/shared/dto/StartAnalysisCompetitorDTO.java b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartAnalysisCompetitorDTO.java index 42c2acd9715..db6c345e84d 100644 --- a/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartAnalysisCompetitorDTO.java +++ b/java/com.sap.sailing.dashboards.gwt/src/main/java/com/sap/sailing/dashboards/gwt/shared/dto/StartAnalysisCompetitorDTO.java @@ -2,6 +2,7 @@ package com.sap.sailing.dashboards.gwt.shared.dto; import java.io.Serializable; +import com.sap.sailing.domain.common.dto.BoatDTO; import com.sap.sailing.domain.common.dto.CompetitorDTO; public class StartAnalysisCompetitorDTO implements Serializable{ @@ -9,5 +10,6 @@ public class StartAnalysisCompetitorDTO implements Serializable{ private static final long serialVersionUID = -4236444628127858568L; public CompetitorDTO competitorDTO; + public BoatDTO boatDTO; public StartAnalysisRankingTableEntryDTO rankingTableEntryDTO; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceCompetitorSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceCompetitorSelectionModel.java index dd58672ea10..4ae12ddef73 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceCompetitorSelectionModel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceCompetitorSelectionModel.java @@ -16,6 +16,12 @@ public class RaceCompetitorSelectionModel extends CompetitorSelectionModel imple boatsOfCompetitors = new HashMap<>(); } + public RaceCompetitorSelectionModel(boolean hasMultiSelection, Map boatsOfCompetitors) { + super(hasMultiSelection); + this.boatsOfCompetitors = boatsOfCompetitors; + addAll(boatsOfCompetitors.keySet()); + } + public RaceCompetitorSelectionModel(boolean hasMultiSelection, CompetitorColorProvider competitorColorProvider, Map boatsOfCompetitors) { super(hasMultiSelection, competitorColorProvider); this.boatsOfCompetitors = boatsOfCompetitors; From 13daaab4b843a53701c73feb413d3fc567d403e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steffen=20Sch=C3=A4fer?= Date: Tue, 14 Aug 2018 10:51:40 +0200 Subject: [PATCH 43/58] Code formatting change only --- .../sailing/gwt/ui/client/RaceCompetitorSelectionModel.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/RaceCompetitorSelectionModel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceCompetitorSelectionModel.java index 4ae12ddef73..61ce0985489 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceCompetitorSelectionModel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceCompetitorSelectionModel.java @@ -35,11 +35,11 @@ public class RaceCompetitorSelectionModel extends CompetitorSelectionModel imple @Override public void setBoat(CompetitorDTO competitor, BoatDTO boat) { - boatsOfCompetitors.put(competitor, boat); + boatsOfCompetitors.put(competitor, boat); } @Override public BoatDTO getBoat(CompetitorDTO competitor) { - return boatsOfCompetitors.get(competitor); + return boatsOfCompetitors.get(competitor); } } From 20b061cefe31563229939116df9e83b353d0f830 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 14 Aug 2018 15:46:22 +0200 Subject: [PATCH 44/58] trim hostnames during log file analysis Change-Id: I798aa4658bdb88d72c413b1c3479a986095ca306 --- java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java b/java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java index 01f885db337..42aa9396098 100755 --- a/java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java +++ b/java/com.sap.sse/src/com/sap/sse/util/apachelog/LogEntry.java @@ -36,7 +36,7 @@ public class LogEntry { public LogEntry(String logEntry) { final Iterator i = Util.splitAlongWhitespaceRespectingDoubleQuotedPhrases(logEntry).iterator(); - hostname = i.next(); + hostname = i.next().trim(); requestorIpString = i.next(); remoteLogname = i.next(); remoteUser = i.next(); From 019b87c9c6ffe0ba2690e8b2000276346b82e98d Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Tue, 14 Aug 2018 18:30:24 +0200 Subject: [PATCH 45/58] started a Wiki document on our goals and plans for a central orchestrator Change-Id: Ia8c40e6f6a1eb1cc0e207dc04e5f5842dc288ec4 --- wiki/projects/cloud-orchestrator.md | 132 ++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100755 wiki/projects/cloud-orchestrator.md diff --git a/wiki/projects/cloud-orchestrator.md b/wiki/projects/cloud-orchestrator.md new file mode 100755 index 00000000000..287dad5cc9e --- /dev/null +++ b/wiki/projects/cloud-orchestrator.md @@ -0,0 +1,132 @@ +# Cloud Orchestration - Project Plan + +[TOC] + +The cloud landscape used to run the SAP Sailing Analytics as well as its development infrastructure has grown increasingly comprehensive and elaborate over the last few years. It has largely been configured through manual efforts on behalf of event and infrastructure operators and administrators. + +With a growing number of events---be it through sailing leagues or championship events in one or more classes---and with a plan to scale the SAP Sailing Analytics to a broader audience through the use of the SAP Race Manager and the SAP Sail InSight apps, more automation is required for the cloud infrastructure. + +We believe that a central *orchestrator* approach should be used to solve this challenge. Such an orchestrator, a bit like outlined in the figure below, will accept requests immediately from authenticated and authorized users through a Web interface, as well as from processes running on other servers in the cloud, through REST APIs on behalf of authenticated and authorized users. + +![](https://wiki.sapsailing.com/wiki/images/orchestration/architecture.png) + +## Orchestrator Architecture + +In order to authenticate and authorize user requests the orchestrator will benefit from a powerful security infrastructure. The *Shiro* framework that is being used by the SAP Sailing Analytics has proven to be sufficiently configurable, powerful and extensible to support all our needs. It seems desirable to share the security service between the orchestrator and the application servers in the landscape. This will allow us to extend landscape-related permissions to users that can already be authenticated by the application and which can then make requests to the orchestrator, such as providing a new event with a new dedicated database, install a sub-domain prefix with the corresponding load balancer settings, or move an event to a dedicated server/replica cluster with its own scalability limits based on the user's credentials. + +There will also need to be a fair amount of negotiations between the application instances and the orchestrator, relating domain aspects such as event dates, numbers of competitors, number of races to be tracked and the number of regattas, with infrastructure provisioning aspects such as server and network capacities required to support the application set-up. + +### Java OSGi + +This, together with the skill set profile and the set of components found in our team, suggests the use of the architecture that proved successful during the construction of the SAP Sailing Analytics themselves. The orchestrator shall be developed as a Java OSGi application that uses the existing *com.sap.sse...* bundles for basic aspects such as security (Shiro), replication, mail services and replication (e.g., for replicating the central SecurityService with its user store). + +The orchestrator could become the host for the central user store / SecurityService that other application instances replicate by means of partial replication. + +Managing an Amazon Web Services (AWS) Elastic Cloud Computing (EC2) landscape will be carried out through Amazon's AWS Java SDK. + +### Google Web Toolkit (GWT) + +The Web UI of the orchestrator shall be built using the Google Web Toolkit (GWT), using the AdminConsole components from *com.sap.sse.gwt.adminconsole* as well as further GWT UI support from *com.sap.sse.gwt*. This will also allow us to use the *UserManagementPanel* in the administration UI. + +### JAX-RS for REST + +The REST APIs will be built according to the same patterns of OSGi Web Bundles that use JAX-RS as the implementation pattern for the servlets implementing the APIs. Other than for the grown application domain model, here we may choose to use the *Jackson* framework in a controlled way to pass structured information back and forth between application instances and the orchestrator. + +The REST APIs will use the existing authentication/authorization realm implementation that we use already in the application in just the same way that the security-enabled REST endpoints that already exist for the SAP Sailing Analytics do. + +### In-Memory with Write-Through Persistence with MongoDB + +A running orchestrator instance shall not need to access a persistence layer for reading requests. Just like the applications, the orchestrator has to be able to launch / re-launch with an already established landscape. In this case, the orchestrator will need to be able to find out about the current status of the landscape it is expected to orchestrate. Instead of having to explore the world-wide cloud landscape to find instances the orchestrator may be expected to orchestrate, a persistence layer will help it to recover the last managed status. + +Instead of introducing another persistence architecture, the orchestrator should use the existing MongoDB persistence pattern. It has proven useful for the scenarios we have in mind. The orchestrator will not have to deal with excessive write loads, and data volumes we expect to remain in the few megabytes range at most as all these data need to describe are the instances and their roles. + +### Logging + +The orchestrator needs to log in detail on behalf of which user which landscape interaction has taken place. In particular, this must include all cost-driving interactions such as launching instances, and creating load balancers. The log can be a *java.util.logging* (*jul*) log where the set-up needs to ensure that no log is ever lost upon rotation. www.sapsailing.com:/var/log/old could be the host for those logs as it is the ever-growing place for all sorts of logs we never want to lose. + +## Orchestration Elements + +### Application Load Balancers (ALBs) + +### ALB Rules + +### Target Groups + +### DNS Record Sets + +### Apache Web Servers + +The landscape relies on Apache *httpd* web servers mostly for two reasons: + +- consolidated, harmonized logging of web requests + +- powerful reverse proxying with redirect macros and SSL support + +All httpd instances have common elements for SSL certificate configuration as well as a set of redirect and rewrite macros that are aware of the application and its URL configurations, for example Plain-SSL-Redirect, Home-SSL-Redirect, Series-SSL-Redirect, and Event-SSL-Redirect. These macros can then be used to rewrite requests made for a base URL such as *worlds2018.sapsailing.com* to the corresponding event landing page. + +Other macros set up end points for server monitoring (*/internal-server-status*) and for the health checks performed by the load balancer's target group which uses the host's internal IP address as the request server name. + +The interface for such a web server will need to allow the orchestrator to add and remove such rewrite macro usages, configure the macro parameters such as the event ID to use for the Event-SSL-Redirect macro, and to tell the *httpd* process to re-load its configuration to make any changes effective. + +### Java Application Instances and their Health + +Application nodes have to provide a REST API with reliable health information. + +A replica is not healthy while its initial load is about to start, or is still on-going or its replication queue is yet to drain after the initial load has finished. A replica will take harm from requests received while the initial load is received or being processed. Requests may be permitted after the initial load has finished processing and while the replication queue is being drained, although the replica will not yet have reached the state that the master is in. + +A master is not healthy until it has finished loading all data from the MongoDB during the activation of the *RacingEventService*. It will take harm from requests received during this loading phase. After loading the "master data," a master server will try to restore all race trackers, starting to track the races for which a so-called "restore record" exists. During this phase the master is not fully ready yet but will not take harm from requests after loading all master data has completed. For example, in an emergency situation where otherwise the replication cluster would be unavailable it may be useful to already direct requests at such a master to at least display a landing page or a meaningful error page. + +### Multi-Instances + +A "multi-instance" is a single EC2 host that runs several application processes that share the physical memory and a large locally-attached SSD (typically 2TB or more) used for swap space. Each of these processes has its own configuration in an *env.sh* file which provides the process with a server name, the amount of heap space memory to reserve, a specific MongoDB configuration (DB name, host, port), a RabbitMQ configuration in case the process shall scale with replicas, a port through which the embedded Jetty web server will be reachable, a telnet port at which the OSGi console registers itself, as well as a UDP port for the Expedition connector. + +Each server process has its own directory under */home/sailing/servers* that is named after the server name. Should a multi-instance host be re-booted, it will launch all application processes it finds under */home/sailing/servers*. This is important to keep in mind for the scenario of temporarily or permanently migrating a node from a multi-instance set-up to a dedicated replication cluster set-up. + +Scaling a multi-instance with replication has not yet been exercised. It may pose a few new challenges due to the replicas likely serving the application on a standard port (usually *8888*) as opposed to the node on the multi-instance host which uses a different port. In this case, separate target groups for the master and the replicas will be required, and the master will not be able to register with the public-facing target group which then only can contain replicas. + +### Dedicated Instances + +A dedicated server will typically be used for an event whose size in terms of competitors and races tracked and the number of users concurrently watching those races live exceeds what we think a multi-instance set-up can reasonably carry. Dedicated CPU and memory resources will ensure that such a live event runs smoothly and can scale elastically, independent of other nodes that would be running on the same host in a multi-instance set-up. + +As in the multi-instance case, replication can be used for dedicated instances. Here, since the dedicated instance will also run on the default port, optionally the master and the replica instances can all be put into the public-facing load balancer target group. + +A dedicated instance will grab the amount of RAM it reasonably can with the physical RAM provided by the EC2 host it is deployed to. Its HTTP port will default to 8888, the telnet port to 14888 and the Expedition UDP port to 2010. Like a multi-instance it has a server name as well as a MongoDB and RabbitMQ configuration. + +### Master Data Import (MDI) + +An instance can run a Master Data Import (MDI) for one or more leaderboard groups from another server instance. The typical scenario is that of importing an event into the archive server, but other scenarios may be conceived. + +We will need a secured API to trigger an MDI on a server instance. Beyond this, connectivity data needs to be obtained from the exporting server so that all tracked races can be restored on the importing server. Afterwards, a comparison has to be made, ensuring that all data has arrived correctly on the importing side. + +Part of such a scenario can also be to manage the remote server references on the importing server: should the importing instance be the archive server and should the archive server have had a remote server reference to the exporting server, this reference can be cleared after the import has succeeded. + +### Archive Servers + +### Sharding + +### MongoDB Databases + +### RabbitMQ Servers and Exchanges + +## Orchestration Use Cases + +### Create a New Event on a Dedicated Replication Cluster + +### Create a New "Club Set-Up" in a Multi-Instance + +### Migrate from a Multi-Instance to a Dedicated Replication Cluster + +### Migrate from a Dedicated Replication Cluster into a Multi-Instance + +### Observe and Automatically Scale a Replication Cluster + +### Archiving + +### Archive Server Upgrade and Switch + +### Automatic Archive Server Fail-Over + +### Configure Sharding in a Replication Cluster + +### Amazon Machine Image (AMI) Upgrades + From 165576dbcdc496cf802a21a6a1179e572595d3da Mon Sep 17 00:00:00 2001 From: Alessandro Stoltenberg Date: Tue, 14 Aug 2018 18:41:32 +0200 Subject: [PATCH 46/58] Made installPluginsForEclipsePhoton.sh executable. --- configuration/installPluginsForEclipsePhoton.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 configuration/installPluginsForEclipsePhoton.sh diff --git a/configuration/installPluginsForEclipsePhoton.sh b/configuration/installPluginsForEclipsePhoton.sh old mode 100644 new mode 100755 From 784a47e77c742717945e6623d54ff054f251e93a Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 15 Aug 2018 10:33:10 +0200 Subject: [PATCH 47/58] removed extra empty lines Change-Id: Id0f148452c6b45a4abe1964185b6d84f2aab9df2 --- .../sap/sailing/domain/persistence/impl/CollectionNames.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java index 19329ceb66f..626c339eb85 100755 --- a/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java +++ b/java/com.sap.sailing.domain.persistence/src/com/sap/sailing/domain/persistence/impl/CollectionNames.java @@ -68,8 +68,6 @@ public enum CollectionNames { */ REGATTA_FOR_RACE_ID, - - /** * Legacy store for competitors before implementation of bug2822 * Contains the old competitors with contained boats. From 82b8dcf17e6336f9af6471ccfd62d8d71428d458 Mon Sep 17 00:00:00 2001 From: Robin Fleige Date: Wed, 15 Aug 2018 11:10:59 +0000 Subject: [PATCH 48/58] Updated onboarding (markdown) --- wiki/howto/onboarding.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wiki/howto/onboarding.md b/wiki/howto/onboarding.md index afbd2b303da..13cee1c3a9d 100644 --- a/wiki/howto/onboarding.md +++ b/wiki/howto/onboarding.md @@ -14,7 +14,9 @@ First of all, make sure you've looked at [http://www.amazon.de/Patterns-Elements 4. MongoDB (e.g. Production Release 2.6.7), download: [https://www.mongodb.com/](https://www.mongodb.com/) 5. RabbitMQ, download from [http://www.rabbitmq.com](http://www.rabbitmq.com). Requires Erlang to be installed. RabbitMQ installer will assist in installing Erlang. Some sources report that there may be trouble with latest versions of RabbitMQ. In some cases, McAffee seems to block the installation of the latest version on SAP hardware; in other cases connection problems to newest versions have been reported. We know that version 3.6.8 works well. [https://github.com/rabbitmq/rabbitmq-server/releases/tag/rabbitmq_v3_6_8](https://github.com/rabbitmq/rabbitmq-server/releases/tag/rabbitmq_v3_6_8) is the link. 6. JDK 1.7 (Java SE 7), [http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html](http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html) +6.2 Alternatively you can use the SAPJVM 1.7 [http://sapjvm.wdf.sap.corp:1080/downloads](http://sapjvm.wdf.sap.corp:1080/downloads), select JVM 1.7. Extract the downloaded .zip into desired location (e.g. C:\Program Files\Java), then Go to Window -> Preferences -> Java -> Installed JREs and add the VM 7. JDK 1.8 (Java SE 8), [http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) +7.2 Alternatively you can use the SAPJVM 1.8 [http://sapjvm.wdf.sap.corp:1080/downloads](http://sapjvm.wdf.sap.corp:1080/downloads), select JVM 1.8. Extract the downloaded .zip into desired location (e.g. C:\Program Files\Java), then Go to Window -> Preferences -> Java -> Installed JREs and add the VM 8. Maven 3.1.1 (or higher), [http://maven.apache.org](http://maven.apache.org) 9. GWT SDK 2.8.2 ([http://www.gwtproject.org/download.html](http://www.gwtproject.org/download.html)) 10. Standalone Android SDK (see section "Additional steps required for Android projects"). OPTIONALLY: You may additionally install Android Studio ([https://developer.android.com/tools/studio/index.html](https://developer.android.com/tools/studio/index.html)) or IntelliJ IDEA ([https://www.jetbrains.com/idea/download/](https://www.jetbrains.com/idea/download/)). @@ -119,6 +121,7 @@ Copy the settings.xml **and** the toolchains.xml from the top-level git folder t #### Further hints - Configure Eclipse to use Chrome or Firefox as the default browser - Install the GWT Browser Plugin for the GWT Development mode (Chrome or Firefox; as of this writing (2013-11-05), Firefox is the only platform where the plug-in runs stably) (As of 2016-08-31 Firefox is the only browser supporting the GWT plugin, you have to download Firefox version 24 for it to work) +- Use SAP JVM Profiler. If you used the script above and installed the SAPJVM instead of the jdk, you can now open the profiling perspective by clicking on Window -> Perspective -> Open Perspective -> Profiling) #### Git usage troubleshooting From 549709b8b8c1e00c6eaf1b025adfd018c556e649 Mon Sep 17 00:00:00 2001 From: Robin Fleige Date: Wed, 15 Aug 2018 12:23:22 +0000 Subject: [PATCH 49/58] Updated onboarding (markdown) --- wiki/howto/onboarding.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/wiki/howto/onboarding.md b/wiki/howto/onboarding.md index 13cee1c3a9d..eaa122384b4 100644 --- a/wiki/howto/onboarding.md +++ b/wiki/howto/onboarding.md @@ -13,10 +13,8 @@ First of all, make sure you've looked at [http://www.amazon.de/Patterns-Elements 3. Git (e.g. Git for Windows v2.18), [http://git-scm.com](http://git-scm.com) / [https://git-for-windows.github.io](https://git-for-windows.github.io) 4. MongoDB (e.g. Production Release 2.6.7), download: [https://www.mongodb.com/](https://www.mongodb.com/) 5. RabbitMQ, download from [http://www.rabbitmq.com](http://www.rabbitmq.com). Requires Erlang to be installed. RabbitMQ installer will assist in installing Erlang. Some sources report that there may be trouble with latest versions of RabbitMQ. In some cases, McAffee seems to block the installation of the latest version on SAP hardware; in other cases connection problems to newest versions have been reported. We know that version 3.6.8 works well. [https://github.com/rabbitmq/rabbitmq-server/releases/tag/rabbitmq_v3_6_8](https://github.com/rabbitmq/rabbitmq-server/releases/tag/rabbitmq_v3_6_8) is the link. -6. JDK 1.7 (Java SE 7), [http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html](http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html) -6.2 Alternatively you can use the SAPJVM 1.7 [http://sapjvm.wdf.sap.corp:1080/downloads](http://sapjvm.wdf.sap.corp:1080/downloads), select JVM 1.7. Extract the downloaded .zip into desired location (e.g. C:\Program Files\Java), then Go to Window -> Preferences -> Java -> Installed JREs and add the VM -7. JDK 1.8 (Java SE 8), [http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) -7.2 Alternatively you can use the SAPJVM 1.8 [http://sapjvm.wdf.sap.corp:1080/downloads](http://sapjvm.wdf.sap.corp:1080/downloads), select JVM 1.8. Extract the downloaded .zip into desired location (e.g. C:\Program Files\Java), then Go to Window -> Preferences -> Java -> Installed JREs and add the VM +6. JDK 1.7 (Java SE 7), [http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html](http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html) --- Alternatively you can use the SAPJVM 1.7: Go to [http://sapjvm.wdf.sap.corp:1080/downloads](http://sapjvm.wdf.sap.corp:1080/downloads), select JVM 1.7, extract the downloaded .zip into desired location (e.g. C:\Program Files\Java), then Go to Window -> Preferences -> Java -> Installed JREs and add the VM. +7. JDK 1.8 (Java SE 8), [http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) --- Alternatively you can use the SAPJVM 1.8: Go to [http://sapjvm.wdf.sap.corp:1080/downloads](http://sapjvm.wdf.sap.corp:1080/downloads), select JVM 1.8, extract the downloaded .zip into desired location (e.g. C:\Program Files\Java), then Go to Window -> Preferences -> Java -> Installed JREs and add the VM. 8. Maven 3.1.1 (or higher), [http://maven.apache.org](http://maven.apache.org) 9. GWT SDK 2.8.2 ([http://www.gwtproject.org/download.html](http://www.gwtproject.org/download.html)) 10. Standalone Android SDK (see section "Additional steps required for Android projects"). OPTIONALLY: You may additionally install Android Studio ([https://developer.android.com/tools/studio/index.html](https://developer.android.com/tools/studio/index.html)) or IntelliJ IDEA ([https://www.jetbrains.com/idea/download/](https://www.jetbrains.com/idea/download/)). From c8b555b599e2682ff1338d6ef264c9385dac30ba Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 15 Aug 2018 15:27:39 +0200 Subject: [PATCH 50/58] fixed typo in finduserbyemail script Change-Id: I5575c9c11be3bd8c1a5efe8e441474e87613a219 --- configuration/finduserbyemail.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/finduserbyemail.sh b/configuration/finduserbyemail.sh index 87468208813..62bea8c2288 100755 --- a/configuration/finduserbyemail.sh +++ b/configuration/finduserbyemail.sh @@ -3,7 +3,7 @@ # Executes the following script on the DB server: ##!/bin/sh #useremail=$1 -#for i in `echo "show dbs" | /opt/mongodb-linux-x86_64-2.6.7/bin/mongo --port 10202 | grep -v "^bye$" | awk '{print $1;'}`; do +#for i in `echo "show dbs" | /opt/mongodb-linux-x86_64-2.6.7/bin/mongo --port 10202 | tail -n +3 | grep -v "^bye$" | awk '{print $1;}'`; do # match=`echo "db.USERS.find({EMAIL: '$useremail'}); db.COMPETITORS.find({email: '$useremail'})" | /opt/mongodb-linux-x86_64-2.6.7/bin/mongo --quiet --port 10202 $i` # if [ "$match" != "" ]; then # echo ${i}: $match From e2149eec0fab101d9dc3cd2359fedd584a41d1db Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 15 Aug 2018 18:35:46 +0200 Subject: [PATCH 51/58] a modified backup script that backs up all DBs except "local" Change-Id: I877040de3f1ce64898386e4ba3a944f5112dca6a --- configuration/backup_full.sh | 121 +++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100755 configuration/backup_full.sh diff --git a/configuration/backup_full.sh b/configuration/backup_full.sh new file mode 100755 index 00000000000..d48b0d05f4d --- /dev/null +++ b/configuration/backup_full.sh @@ -0,0 +1,121 @@ +#!/bin/sh + +# This is a template for creating backup of data and persist them +# to a central backup server. Make sure to adapt this script to your needs. +# +# Please do not change the name of the bakup (bup save -n) because that can +# have strange effects. If you change it you absolutely need to make sure that +# only the same root directories are associated to that name. It is NOT possible +# to backup different root directories to the same backup name. +# +# Maintainer: simon.marcel.pamies@sap.com, axel.uhl@sap.com + +# Directories to backup +BACKUP_DIRECTORIES="/etc /var/log/mongodb" + +# Prefix for backup - set that to the hostname of your server +# Make sure to change this! +PREFIX="database" + +# Directory for temporary files +TARGET_DIR=/var/lib/mysql/backup + +# Configuration for MongoDB backup +MONGO_PORTS="10202 10201" +MONGO_INSTALLATION=/opt/mongodb-linux-x86_64-2.6.7 +MONGO_CMD=$MONGO_INSTALLATION/bin/mongo +MONGOEXPORT_CMD=$MONGO_INSTALLATION/bin/mongoexport +MONGODB_COLLECTION_WIND="WIND_TRACKS" +MONGODB_COLLECTION_LOCAL="local" + +# Configuration for MySQL +MYSQL_DATABASES="bugs mysql" +MYSQLEXPORT_CMD="mysqldump -u root --password=sailaway" + +# Configuration for external backup server - needs a ssh key +BACKUP_SERVER="backup@172.31.25.136" + +# Set date for this backup - this makes it possible to restore +# files from different branches (named backups) +BACKUP_DATE=`date +%s` + +# Aliases +BUP_CMD="/opt/bup/bup" +BUP_ADDITIONAL="-r $BACKUP_SERVER:/home/backup/$PREFIX" +BUP_IGNORES='--exclude-rx=/war/$' +BUP_CMD_INDEX="$BUP_CMD index $BUP_IGNORES" +BUP_CMD_SAVE="$BUP_CMD save --date=$BACKUP_DATE $BUP_ADDITIONAL" + +PARAM=$@ + +# Make sure to install the right libraries +yum install python-devel fuse-python pyxattr pylibacl perl-Time-HiRes + +# We need to set HOME in case it isn't set right - assuming root is calling +export HOME=/root + +# Make sure to init local and remote repository +# It is perfectly safe to reinitialize a repository w/o overwriting data +$BUP_CMD init +ssh $BACKUP_SERVER "/opt/bup/bup init -r /home/backup/$PREFIX" + +# Backup general directories +for OTHER_DIR in $BACKUP_DIRECTORIES; do + NORMALIZED_DIR_NAME=${OTHER_DIR//\//-} + echo "Backing up $OTHER_DIR" + $BUP_CMD_INDEX $OTHER_DIR + $BUP_CMD_SAVE -n dir$NORMALIZED_DIR_NAME $OTHER_DIR + # no need to remove any backup files here +done + +# ************************************** +# Start of database specific backup +# ************************************** + +# Export MongoDB instances with wind upon request (weekly parameter provided) +for MONGO_PORT in $MONGO_PORTS; do + echo Backing up MongoDB running on port $MONGO_PORT + MONGODB_DATABASES=`echo "show dbs" | $MONGO_CMD --port $MONGO_PORT | tail -n +3 | grep -v "^bye$" | awk '{print $1;}'` + for i in $MONGODB_DATABASES; do + echo " Backing up database $i in MongoDB running on port $MONGO_PORT" + MONGODB_COLLECTIONS=`echo "use $i +show collections" | $MONGO_CMD --port $MONGO_PORT | tail -n +4 | grep -v "^system.indexes$" | grep -v "^bye$"` + echo "Collections in MongoDB $i running on port $MONGO_PORT: $MONGODB_COLLECTIONS" + for j in $MONGODB_COLLECTIONS; do + if [ "$j" = "$MONGODB_COLLECTION_WIND" ]; then + WIND_SUFFIX="-wind" + else + WIND_SUFFIX= + fi + if [ "$i" != "winddb" ]; then + DB_DIR=$TARGET_DIR/mongodb-$MONGO_PORT-$i$WIND_SUFFIX + else + DB_DIR=$TARGET_DIR/mongodb-$MONGO_PORT$WIND_SUFFIX + fi + if [ "$i" != "$MONGODB_COLLECTION_LOCAL" -a ( "$i" != "$MONGODB_COLLECTION_WIND" -o "$PARAM" = "weekly" ) ]; then + echo " Backing up collection $j in MongoDB $i running on port $MONGO_PORT" + echo "$MONGOEXPORT_CMD --port $MONGO_PORT -d $i -c $j > $DB_DIR/$j.json" + $MONGOEXPORT_CMD --port $MONGO_PORT -d $i -c $j > $DB_DIR/$j.json + fi + done + done +done + +# Export MySQL dump for selected databases +for MYSQL_DB in $MYSQL_DATABASES; do + mkdir $TARGET_DIR/mysql + echo "$MYSQLEXPORT_CMD $MYSQL_DB > $TARGET_DIR/mysql/$MYSQL_DB.sql" + $MYSQLEXPORT_CMD $MYSQL_DB > $TARGET_DIR/mysql/$MYSQL_DB.sql +done + +# Backup the whole thing now and push to external server +$BUP_CMD_INDEX $TARGET_DIR +$BUP_CMD_SAVE -n databases $TARGET_DIR/ + +# clean up +find $TARGET_DIR -name *.json | xargs rm -f +rm -f $TARGET_DIR/mysql/*.sql + +# ************************************** +# End of database specific backup +# ************************************** From b451c96b345f6cad572f97f7de12bbe1cac7580e Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 15 Aug 2018 18:36:58 +0200 Subject: [PATCH 52/58] fixed parentheses problem in backup script Change-Id: I63b8db70d6222914855722ccafe52f2a8d368383 --- configuration/backup_full.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration/backup_full.sh b/configuration/backup_full.sh index d48b0d05f4d..e016b0b9d8e 100755 --- a/configuration/backup_full.sh +++ b/configuration/backup_full.sh @@ -92,7 +92,7 @@ show collections" | $MONGO_CMD --port $MONGO_PORT | tail -n +4 | grep -v "^syste else DB_DIR=$TARGET_DIR/mongodb-$MONGO_PORT$WIND_SUFFIX fi - if [ "$i" != "$MONGODB_COLLECTION_LOCAL" -a ( "$i" != "$MONGODB_COLLECTION_WIND" -o "$PARAM" = "weekly" ) ]; then + if [ "$i" != "$MONGODB_COLLECTION_LOCAL" -a \( "$i" != "$MONGODB_COLLECTION_WIND" -o "$PARAM" = "weekly" \) ]; then echo " Backing up collection $j in MongoDB $i running on port $MONGO_PORT" echo "$MONGOEXPORT_CMD --port $MONGO_PORT -d $i -c $j > $DB_DIR/$j.json" $MONGOEXPORT_CMD --port $MONGO_PORT -d $i -c $j > $DB_DIR/$j.json From f8ee11dadd18c6cec6c2335609f5e77ed2514d6a Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 15 Aug 2018 18:38:26 +0200 Subject: [PATCH 53/58] create backup directory before running mongoexport Change-Id: I0a82219a31c98e5260ad84ae0fc3f121d0ec5015 --- configuration/backup_full.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/configuration/backup_full.sh b/configuration/backup_full.sh index e016b0b9d8e..afca4a1c32c 100755 --- a/configuration/backup_full.sh +++ b/configuration/backup_full.sh @@ -95,6 +95,7 @@ show collections" | $MONGO_CMD --port $MONGO_PORT | tail -n +4 | grep -v "^syste if [ "$i" != "$MONGODB_COLLECTION_LOCAL" -a \( "$i" != "$MONGODB_COLLECTION_WIND" -o "$PARAM" = "weekly" \) ]; then echo " Backing up collection $j in MongoDB $i running on port $MONGO_PORT" echo "$MONGOEXPORT_CMD --port $MONGO_PORT -d $i -c $j > $DB_DIR/$j.json" + mkdir -p $DB_DIR $MONGOEXPORT_CMD --port $MONGO_PORT -d $i -c $j > $DB_DIR/$j.json fi done From e9af9a2a0369da3a1c8d179fc94b4f9bda59d85b Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 15 Aug 2018 20:48:38 +0000 Subject: [PATCH 54/58] Updated cloud orchestrator (markdown) --- wiki/projects/cloud-orchestrator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 wiki/projects/cloud-orchestrator.md diff --git a/wiki/projects/cloud-orchestrator.md b/wiki/projects/cloud-orchestrator.md old mode 100755 new mode 100644 index 287dad5cc9e..4f8ffb8ce05 --- a/wiki/projects/cloud-orchestrator.md +++ b/wiki/projects/cloud-orchestrator.md @@ -1,6 +1,6 @@ # Cloud Orchestration - Project Plan -[TOC] +[[_TOC_]] The cloud landscape used to run the SAP Sailing Analytics as well as its development infrastructure has grown increasingly comprehensive and elaborate over the last few years. It has largely been configured through manual efforts on behalf of event and infrastructure operators and administrators. From 1b8451cd68f185fd92212ca1da1f08211c34a1a6 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 16 Aug 2018 09:17:37 +0200 Subject: [PATCH 55/58] continued on orchestrator write-up Change-Id: Iff4963db809e4c0a07276fe329dd4778f52202c5 --- wiki/projects/cloud-orchestrator.md | 64 ++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/wiki/projects/cloud-orchestrator.md b/wiki/projects/cloud-orchestrator.md index 287dad5cc9e..4949a64a6df 100755 --- a/wiki/projects/cloud-orchestrator.md +++ b/wiki/projects/cloud-orchestrator.md @@ -48,12 +48,30 @@ The orchestrator needs to log in detail on behalf of which user which landscape ### Application Load Balancers (ALBs) -### ALB Rules +Up to 20 application load balancers (ALBs) can be used in a single AWS region. Each of them can have up to 100 rules that map requests to a target group based on hostname and URL path patterns. We use a default ALB that is the CNAME target for **.sapsailing.com*, and as such allows the orchestrator to add rules to it without the need to add new DNS records to Route53. Dedicated ALBs can be used in addition to circumvent the limit of 100 rules per ALB. To ensure those dedicated ALBs are targeted by user requests, DNS records for the sub-domains that those ALBs have rules for need to be added to Route53. + +All ALBs need default rules that forward requests to the archive server (production or fail-over). + +Currently, several sub-domain names exist (e.g., wiki, bugzilla and hudson) that are also handled by the default ALB, forwarding to our central webserver which then acts as a reverse proxy, using rewrite rules to redirect the clients to the specific hosts running those services. We may consider changing this and introduce DNS records for those service-based sub-domains, mapping them explicitly to the elastic IP of the central webserver. This would allow us to separate the Apache httpd instances for those central services from the httpd instances used for the archive servers. ### Target Groups +Those hold the nodes responding to requests. In a replication cluster we usually want to have one for the "-master" URL that event administrators use + +### Alerts + +Alerts can be defined for different metrics and with different notification targets. SMS text messages and e-mail notifications are available. + +The orchestrator shall be able of managing such alerts. It shall be possible to attach such alerting rules to an entire replication cluster, meaning that all target groups used for managing the cluster shall receive the same monitoring and alerting rule (see also [Sharding](#sharding)). + ### DNS Record Sets +As the number of rules per load balancer is limited to 100 as of now, and only one ALB can be set up to handle **.sapsailing.com*, this ALB can only be used for the volatile, fast-changing sub-domain names and event-driven set-ups, given there are not even too many of those happening concurrently to exceed the limit. + +All other, specifically the longer-running, sub-domains shall be mapped through a dedicated Route53 DNS entry in the *sapsailing.com* hosted zone. Up to 20 ALBs can be created per region, and each of those can have up to 100 rules. This should suffice for some time to come. We should also consider asking users to pay for the special service of a dedicated sub-domain in the future. + +Those dedicated DNS entries then point to a non-default ALB that has the rules for that sub-domain. Those ALBs also default to the archive server and central web server if no other rule matches. Should a sub-domain's content be archived, the DNS entry can be removed. The default rules of both, the dedicated ALB and the default ALB for **.sapsailing.com* will forward requests to the archive server. + ### Apache Web Servers The landscape relies on Apache *httpd* web servers mostly for two reasons: @@ -66,6 +84,8 @@ All httpd instances have common elements for SSL certificate configuration as we Other macros set up end points for server monitoring (*/internal-server-status*) and for the health checks performed by the load balancer's target group which uses the host's internal IP address as the request server name. +All our hosts can run their own Apache httpd process. Currently, the two archive servers don't. Instead, they rely on the single central www.sapsailing.com webserver which hosts all redirect macro usages for all sub-domains that shall be handled by the archive server. This should probably be changed in the future, such that each archive server runs its own httpd process. It would remove the single point of failure that the current central webserver represents and may make room for a more clever set-up where the fail-over archive server is handled by a target group that is the default in the ALB rule set even after the default rule that points at the target group for the production archive server. This way, together with an alarm defined, failover to the secondary archive server could be automatic and instant. + The interface for such a web server will need to allow the orchestrator to add and remove such rewrite macro usages, configure the macro parameters such as the event ID to use for the Event-SSL-Redirect macro, and to tell the *httpd* process to re-load its configuration to make any changes effective. ### Java Application Instances and their Health @@ -102,12 +122,54 @@ Part of such a scenario can also be to manage the remote server references on th ### Archive Servers +We run a production copy of the "archive server" that hosts selected events for which we decide that they have sufficient quality to be promoted through sapsailing.com's landing page. Most events sponsored by SAP fall into this category. The archive server is the target of a "Master Data Import" (MDI) after an event is finished, where data is moved from a dedicated event server into the archive. + +As the archive server requires several hours for a re-start, a failover instance exists that usually runs a release that is not as new as the one on the production archive server. The reason for this choice of release is that in case we run into a regression that tests have not revealed, switching to the failover archive allows us to "revert" to a release that hopefully does not have this regression while we then have some time to fix the issue on the production archive server. + +The failover archive server may be used for special purposes such as tricky data-mining queries that we don't want to affect the performance of the production archive server, or acting as an additional Hudson build slave because its CPUs are usually idling. + +The orchestrator shall be aware of the two archive server instances and shall know which one is the production and which one the fail-over copy. It should have an awareness of the releases installed and should offer the release upgrade as an action that will install the latest release to the fail-over copy, restart it, wait for it to become "healthy" and then compare the contents with the production archive server. If all runs well, routing/ALB/Apache rules can be switched to swap production and fail-over copy. + +There should in the future be dedicated target groups for production and fail-over archive servers that can be used to define an automatic fail-over order in the ALB. A severe alert should be sent out if the production archive target group has no healthy target. + ### Sharding +Requests to the */gwt/service/sailing* RPC service that cause calculations for a specific leaderboard are suffixed with the leaderboard name to which they are specific. Example: */gwt/service/sailing/leaderboard/Sailing_World_Championships_Aarhus_2018___Laser_Radial*. The leaderboard name undergoes escaping of special characters which are then replaced by an underscore character each. + +With this approach, a load balancer can use the URL path with the escaped leaderboard name as criterion in a rule that dispatches requests to target groups based on the leaderboard. This way, although all replicas in a replication cluster maintain an equal memory content, calculations in a replica can be constrained to only a subset of the leaderboards the replica maintains. + +Without this mechanism, all replicas in a replication cluster would be targeted with requests for all leaderboards in a random, round-robin fashion, leading to all replicas running the live calculations for all leaderboards redundantly. As more replicas are added to a replication cluster for a domain with several live leaderboards, using this sharding feature makes more sense because the recalculation load can be split evenly across the replicas. + +A target group for each subset of leaderboards has to be created. The ALB will then have to have a rule for each leaderboard name, deciding the target group to which to route requests for that leaderboard. A default target group for the replication cluster shall exist which catches all other requests and which becomes the default in case a sharding target group runs out of healthy hosts. + ### MongoDB Databases +We currently have a single server node *dbserver.internal.sapsailing.com* on which there are four MongoDB processes running. Three of those are for testing (*dev.sapsailing.com* uses the process listening on port 10201) or legacy purposes, and only one, listening on port 10202, has all the content relevant for the entire production landscape. + +This MongoDB process hosts various MongoDB databases, each with their separate names and separate collections. With the exception of the production and fail-over archive servers which use the *winddb* database, all other master servers use a DB name that should equal that of the server name which usually is a technical short name for the event. Example: *KW2018* represents the server and DB name for the *Kieler Woche 2018* event. + +Replication clusters use a second database that is used by all replicas in the cluster. Right now, replicas don't interact in any well-defined way with the database, and we say that a database accessed by a replica is not in a well-defined state. Replicas don't read from their database which is the reason why this does not matter. For example, in addition to the *KW2018* database there is a *KW2018-replica* database used by all replicas in the *KW2018* replication cluster. + +The MongoDB host currently has a fast EBS volume attached to which it stores all the databases. There is only a single instance in the landscape, making this currently a single point of failure. We should consider changing this. + +The orchestrator shall know about the MongoDB instances we run in the landscape, shall know their ports and hosts and thereby shall be able to monitor in particular the disk volume holding the DB contents. Alerts should be put in place in case those volumes reach critical fill states. In the future, the orchestrator should learn to run MongoDB in various regions, optionally in replicated mode also across regions, making it easier for us to deploy master/replica set-ups across regions. There may even be MongoDB instances in each region that are not part of the cluster that are used only for the phony replica databases. + ### RabbitMQ Servers and Exchanges +Replication is based on RabbitMQ which is used to establish a channel through which the initial load can be transferred safely even under unstable network conditions, and through which the shipping of operations from master to replicas happens, using a "fanout" exchange. + +We currently have only a single RabbitMQ instance in the landscape that hosts all exchanges and all initial load queues. As such, it represents a single point of failure. + +The orchestrator should know about all RabbitMQ instances in the landscape and should know how its exchanges relate to replication clusters, and how its queues relate to the master and replica instances in the landscape. It can thus observe whether all those objects are cleanly removed again when replicas or replication clusters are dismantled and can, if necessary, clean up remnants. + +### Backup Server + +A single EC2 instance called "Backup" with large EBS disk volumes attached hosts various forms of backups. We use *bup*, a variant of *git* capable of dealing well with large binary files. This gives us historizing backups where each version may be fully restored. + +Subject to backup are all production MongoDB databases, the MySQL database content for our Bugzilla server, as well as the file system of the central Webserver, there in particular the configuration and log files. + +The backup script on *dbserver.internal.sapsailing.com:/opt/backup.sh* does *not* backup all MongoDB databases available. I think that it should. See also *configuration/backup_full.sh* in our git repository for a new version. + ## Orchestration Use Cases ### Create a New Event on a Dedicated Replication Cluster From aa10c480640ba025cc63e8f6411521a38396a337 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 16 Aug 2018 09:20:10 +0200 Subject: [PATCH 56/58] removed backup_new.sh; it has been replaced by backup_full.sh Change-Id: I34d22d8395462c136a4306d5d443218a62a6b070 --- configuration/backup_new.sh | 149 ------------------------------------ 1 file changed, 149 deletions(-) delete mode 100755 configuration/backup_new.sh diff --git a/configuration/backup_new.sh b/configuration/backup_new.sh deleted file mode 100755 index ef8d2650da3..00000000000 --- a/configuration/backup_new.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/bin/sh - -# This is a template for creating backup of data and persist them -# to a central backup server. Make sure to adapt this script to your needs. -# -# Please do not change the name of the bakup (bup save -n) because that can -# have strange effects. If you change it you absolutely need to make sure that -# only the same root directories are associated to that name. It is NOT possible -# to backup different root directories to the same backup name. -# -# Maintainer: simon.marcel.pamies@sap.com -# -# ###################################################################### -# CHANGELOG -# -# 2016-09-13 (Steffen Wagner): -# - extracting all databases from a mongo connection instead of defining -# the exportable databases manually -# - made databases excludeable (by regex and name) -# - -# Directories to backup -BACKUP_DIRECTORIES="/etc /var/log/mongodb" - -# Prefix for backup - set that to the hostname of your server -# Make sure to change this! -PREFIX="database" - -# Directory for temporary files -TARGET_DIR=/var/lib/mysql/backup - -# Configuration for MongoDB backup -MONGO_CONNECTIONS="localhost:10201 localhost:10202" -MONGOEXPORT_CMD="/opt/mongodb-linux-x86_64-1.8.1/bin/mongoexport" -MONGODB_COLLECTIONS_NO_WIND="ActiveFileStorageService COMMAND_MESSAGES COMPETITORS CONFIGURATIONS EVENTS FileStorageServiceProperties GPS_FIXES GPS_FIXES_METADATA IGTIMI_ACCESS_TOKENS LAST_MESSAGE_COUNT LEADERBOARDS LEADERBOARD_GROUPS LEADERBOARD_GROUP_LINKS_FOR_EVENTS PREFERENCES RACES_MESSAGES RACE_LOGS REGATTAS REGATTA_FOR_RACE_ID REGATTA_LOGS RESULT_URLS SAILING_SERVERS SETTINGS SWISSTIMING_ARCHIVE_CONFIGURATIONS SWISSTIMING_CONFIGURATIONS TRACTRAC_CONFIGURATIONS USERS VIDEOS" -MONGODB_COLLECTIONS_WIND="WIND_TRACKS" -MONGODB_EXCLUDEDB="10201:LOAD-TEST 10201:winddb 10201:live 10201:local 10201:admin 10202:DNSTEST2 10202:imagetest 10202:live 10202:replication-test-master 10202:load-test 10202:admin 10202:dev 10202:dev-2010 10202:BYC-2010 10202:sapsailinganalytics-dev 10202:test" - -# Configuration for MySQL -MYSQL_DATABASES="bugs mysql" -MYSQLEXPORT_CMD="mysqldump -u root --password=sailaway" - -# Configuration for external backup server - needs a ssh key -BACKUP_SERVER="backup@172.31.25.136" - -# Set date for this backup - this makes it possible to restore -# files from different branches (named backups) -BACKUP_DATE=`date +%s` - -# Aliases -BUP_CMD="/opt/bup/bup" -BUP_ADDITIONAL="-r $BACKUP_SERVER:/home/backup/$PREFIX" -BUP_IGNORES='--exclude-rx=/war/$' -BUP_CMD_INDEX="$BUP_CMD index $BUP_IGNORES" -BUP_CMD_SAVE="$BUP_CMD save --date=$BACKUP_DATE $BUP_ADDITIONAL" - -PARAM=$@ - -# Make sure to install the right libraries -yum install python-devel fuse-python pyxattr pylibacl perl-Time-HiRes - -# We need to set HOME in case it isn't set right - assuming root is calling -export HOME=/root - -# Make sure to init local and remote repository -# It is perfectly safe to reinitialize a repository w/o overwriting data -$BUP_CMD init -ssh $BACKUP_SERVER "/opt/bup/bup init -r /home/backup/$PREFIX" - -# Backup general directories -for OTHER_DIR in $BACKUP_DIRECTORIES; do - NORMALIZED_DIR_NAME=${OTHER_DIR//\//-} - echo "Backing up $OTHER_DIR" - $BUP_CMD_INDEX $OTHER_DIR - $BUP_CMD_SAVE -n dir$NORMALIZED_DIR_NAME $OTHER_DIR - # no need to remove any backup files here -done - -# ************************************** -# Start of database backups -# ************************************** - -# Backup all databases from all mongo connections -for MONGO_DB in $MONGO_CONNECTIONS; do - CONNECTION=(${MONGO_DB//:/ }) - DATABASES_UNFORMATTED=($(mongo --host ${CONNECTION[0]} --port ${CONNECTION[1]} --quiet --eval "JSON.stringify(db.adminCommand('listDatabases'))")) - DATABASES_FORMATTED=`echo ${DATABASES_UNFORMATTED} | jq -r --arg port ${CONNECTION[1]} '.databases| map($port + ":" + .name)|@csv' | tr -d '"' | tr ',' ' '` - - # Remove all explicit excluded databases - LIST_DATABASES_FORMATTED=( $DATABASES_FORMATTED ) - LIST_MONGODB_EXCLUDEDB=( $MONGODB_EXCLUDEDB ) - DATABASES_EXCLUDED=`echo ${LIST_DATABASES_FORMATTED[@]} ${LIST_MONGODB_EXCLUDEDB[@]} | tr ' ' '\n' | sort | uniq -u` - - # Remove all regex excluded databases - DATABASES_FINAL=`echo ${DATABASES_EXCLUDED} | sed -r 's/[0-9]{5}:replica[0-9]+//gi'` - - # Export MongoDB instances with wind upon request (weekly parameter provided) - if [ "$PARAM" = "weekly" ]; then - for MONGO_CONFIG in $DATABASES_FINAL; do - PARTS=(${MONGO_CONFIG//:/ }) - if [ "${PARTS[1]}" != "winddb" ]; then - mkdir $TARGET_DIR/mongodb-${PARTS[0]}-${PARTS[1]}-wind - DB_SUFFIX=${PARTS[0]}-${PARTS[1]}-wind - else - mkdir $TARGET_DIR/mongodb-${PARTS[0]}-wind - DB_SUFFIX=${PARTS[0]}-wind - fi - for COLLECTION in $MONGODB_COLLECTIONS_WIND; do - echo "$MONGOEXPORT_CMD --port ${PARTS[0]} -d ${PARTS[1]} -c $COLLECTION > $TARGET_DIR/mongodb-$DB_SUFFIX/$COLLECTION.json" - $MONGOEXPORT_CMD --port ${PARTS[0]} -d ${PARTS[1]} -c $COLLECTION > $TARGET_DIR/mongodb-$DB_SUFFIX/$COLLECTION.json - done - done - fi - - # Export MongoDB instances without wind - for MONGO_CONFIG in $DATABASES_FINAL; do - PARTS=(${MONGO_CONFIG//:/ }) - if [ "${PARTS[1]}" != "winddb" ]; then - mkdir $TARGET_DIR/mongodb-${PARTS[0]}-${PARTS[1]} - DB_SUFFIX=${PARTS[0]}-${PARTS[1]} - else - mkdir $TARGET_DIR/mongodb-${PARTS[0]} - DB_SUFFIX=${PARTS[0]} - fi - for COLLECTION in $MONGODB_COLLECTIONS_NO_WIND; do - echo "$MONGOEXPORT_CMD --port ${PARTS[0]} -d ${PARTS[1]} -c $COLLECTION > $TARGET_DIR/mongodb-$DB_SUFFIX/$COLLECTION.json" - $MONGOEXPORT_CMD --port ${PARTS[0]} -d ${PARTS[1]} -c $COLLECTION > $TARGET_DIR/mongodb-$DB_SUFFIX/$COLLECTION.json - done - done -done - -# Export MySQL dump for selected databases -for MYSQL_DB in $MYSQL_DATABASES; do - mkdir $TARGET_DIR/mysql - echo "$MYSQLEXPORT_CMD $MYSQL_DB > $TARGET_DIR/mysql/$MYSQL_DB.sql" - $MYSQLEXPORT_CMD $MYSQL_DB > $TARGET_DIR/mysql/$MYSQL_DB.sql -done - -# Backup the whole thing now and push to external server -$BUP_CMD_INDEX $TARGET_DIR -$BUP_CMD_SAVE -n databases $TARGET_DIR/ - -# clean up -find $TARGET_DIR -name *.json | xargs rm -f -rm -f $TARGET_DIR/mysql/*.sql - -# ************************************** -# End of database specific backup -# ************************************** \ No newline at end of file From c94ae19ba1112f0aa3f364fc99d31afade393d0e Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 16 Aug 2018 11:58:02 +0200 Subject: [PATCH 57/58] added to the orchestration write-up Change-Id: I82e4962cb90b40df3308f24a0d7c081ca671c08f --- wiki/projects/cloud-orchestrator.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/wiki/projects/cloud-orchestrator.md b/wiki/projects/cloud-orchestrator.md index d6a28f09e9e..2c1dd770771 100644 --- a/wiki/projects/cloud-orchestrator.md +++ b/wiki/projects/cloud-orchestrator.md @@ -58,6 +58,18 @@ Currently, several sub-domain names exist (e.g., wiki, bugzilla and hudson) that Those hold the nodes responding to requests. In a replication cluster we usually want to have one for the "-master" URL that event administrators use +### Volumes + +There are a number of disk volumes that are critical to monitor and, where necessary, scale. This includes: + +- Backup server (*/home/backup*) +- Database server (*/var/lib/mongodb* and sub-mounts, */var/lib/mysql*) +- Central Webserver (*/var/log/old*, */var/log/old/cache*, */var/www/static*, */var/www/home* hosting the git repository) + +Through the AWS API the read/write throughputs can be observed, and peaks as well as bottlenecks may be identified. At least as importantly, the file system fill state has to be observed which is not possible through the AWS API and needs to happen through the respective instances' operating systems. + +It shall be possible to define alerts based on file systems whose free space drops below a given threshold. Re-sizing volumes automatically may be an interesting option in the future. + ### Alerts Alerts can be defined for different metrics and with different notification targets. SMS text messages and e-mail notifications are available. @@ -96,6 +108,8 @@ A replica is not healthy while its initial load is about to start, or is still o A master is not healthy until it has finished loading all data from the MongoDB during the activation of the *RacingEventService*. It will take harm from requests received during this loading phase. After loading the "master data," a master server will try to restore all race trackers, starting to track the races for which a so-called "restore record" exists. During this phase the master is not fully ready yet but will not take harm from requests after loading all master data has completed. For example, in an emergency situation where otherwise the replication cluster would be unavailable it may be useful to already direct requests at such a master to at least display a landing page or a meaningful error page. +The Java instances have a few and shall have more interesting observable parameters. Some values can be observed through the AWS API, such as general network and CPU loads. So far, the number of leaderboards and the restore process can be observed using JMX managed beans, as can be seen in the JConsole. Other interesting parameters to observe by the orchestrator would be the memory usage and garbage collection (GC) stats, as well as information about the thread pools, their usage and their contention. It would be great if the orchestrator could find out about bottlenecks and how they may be avoided, e.g., hitting bandwidth limitations or not having enough CPUs available to serialize data fast enough for the bandwidth available and the demand observed. + ### Multi-Instances A "multi-instance" is a single EC2 host that runs several application processes that share the physical memory and a large locally-attached SSD (typically 2TB or more) used for swap space. Each of these processes has its own configuration in an *env.sh* file which provides the process with a server name, the amount of heap space memory to reserve, a specific MongoDB configuration (DB name, host, port), a RabbitMQ configuration in case the process shall scale with replicas, a port through which the embedded Jetty web server will be reachable, a telnet port at which the OSGi console registers itself, as well as a UDP port for the Expedition connector. @@ -174,6 +188,8 @@ The backup script on *dbserver.internal.sapsailing.com:/opt/backup.sh* does *not ### Create a New Event on a Dedicated Replication Cluster + + ### Create a New "Club Set-Up" in a Multi-Instance ### Migrate from a Multi-Instance to a Dedicated Replication Cluster From c17ddfb1d9c93b82fb30cdfb797251bf02a1ce61 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 16 Aug 2018 16:04:52 +0200 Subject: [PATCH 58/58] started editing first orchestration use case Change-Id: I987e6bec8562f6a6ea20da22ac63bec71cf0ed13 --- wiki/projects/cloud-orchestrator.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/wiki/projects/cloud-orchestrator.md b/wiki/projects/cloud-orchestrator.md index 2c1dd770771..0fdf6024670 100644 --- a/wiki/projects/cloud-orchestrator.md +++ b/wiki/projects/cloud-orchestrator.md @@ -188,7 +188,23 @@ The backup script on *dbserver.internal.sapsailing.com:/opt/backup.sh* does *not ### Create a New Event on a Dedicated Replication Cluster +A user wants to create a new event. But instead of creating it on an existing server instance, such as the archive server or a club server, he/she would like to create a new dedicated server instance such that the server is a master which later may receive its own replicas and thus form the core of a new replication cluster. The user defines the technical event name, such as *TW2018* for the "Travemünder Woche 2018," which is then used as the server name, MongoDB database name, and replication channel name. +The steps are: + +- Launch a new instance from a prepared AMI (either the AMI is regularly updated with the latest packages and kernel patches, or a "yum update" needs to be run and then the instance rebooted) +- The latest (or a specified) release is installed to */home/sailing/servers/server* +- The */home/sailing/servers/server/env.sh* file is adjusted to reflect the server name, MongoDB settings, as well as the ports to be used (for a dedicated server probably the defaults at 8888 for the HTTP server, 14888 for the OSGi console telnet port, and 2010 for the Expedition UDP connector) +- The Java process is launched +- An event is created; it doesn't necessarily have to have the correct name and attributes yet; it's only important to obtain its UUID. +- With the new security implementation, the event will be owned by the user requesting the dedicated replication cluster, and a new group named after the unique server name is created of which the user is made a part. +- The user obtains a qualified *admin:<servername>* role that grants him/her administrative permissions for all objects owned by the group pertinent to the new replication cluster. +- An Apache *httpd* macro call for the event with its UUID is inserted into a *.conf* file in the instance's */etc/httpd/conf.d* directory +- The Apache *httpd* server is launched +- Two target groups *S-ded-<servername>* and *S-ded-<servername>-master* are created, and the new instance is added to both of them +- Two new ALB rules for *<servername>.sapsailing.com* and *<servername>-master.sapsailing.com* are created, forwarding their requests to the respective target group from the previous step + +asdf ### Create a New "Club Set-Up" in a Multi-Instance