From 4c83d89a9b8e6e9a21aec44d2f32a23f346ec4bf Mon Sep 17 00:00:00 2001 From: "masha.kashirina" Date: Wed, 18 Mar 2026 18:56:13 +0100 Subject: [PATCH 01/10] bug6214: racemap scheduler for avoiding per-competitor refresh --- .../gwt/ui/client/shared/racemap/RaceMap.java | 81 +++++++++++-------- 1 file changed, 46 insertions(+), 35 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 a2a16455485..77cc052e2a3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -3121,6 +3121,48 @@ public class RaceMap extends AbstractCompositeComponent impleme public RaceMapSettings getSettings() { return settings; } + private boolean selectionDependentRefreshScheduled; + private boolean selectionDependentRefreshPending; + + private void scheduleSelectionDependentTailAndMapStateRefresh() { + selectionDependentRefreshPending = true; + + if (!selectionDependentRefreshScheduled) { + selectionDependentRefreshScheduled = true; + Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { + @Override + public void execute() { + selectionDependentRefreshScheduled = false; + + if (selectionDependentRefreshPending) { + selectionDependentRefreshPending = false; + refreshSelectionDependentTailAndMapState(); + } + } + }); + } + } + private void refreshSelectionDependentTailAndMapState() { + if (selectedDetailType != null && !selectedDetailTypeChanged) { + // assumes that the detail values have already been loaded, as the detail type hasn't changed + fixesAndTails.updateDetailValueBoundaries(competitorSelection.getSelectedCompetitors()); + } + // update tails for all competitors because selection change may also affect all unselected competitors + for (CompetitorDTO oneOfAllCompetitors : competitorSelection.getAllCompetitors()) { + Colorline tail = fixesAndTails.getTail(oneOfAllCompetitors); + if (tail != null) { + ColorlineOptions newOptions = + createTailStyle(oneOfAllCompetitors, displayHighlighted(oneOfAllCompetitors)); + tail.setOptions(newOptions); // depends on the min/max boundaries computed above + } + } + // Trigger auto-zoom if needed + final RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); + if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { + zoomMapToNewBounds(zoomSettings.getNewBounds(this)); + } + redraw(); + } @Override public void addedToSelection(CompetitorDTO competitor) { @@ -3148,24 +3190,7 @@ public class RaceMap extends AbstractCompositeComponent impleme showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); } } - if (selectedDetailType != null && !selectedDetailTypeChanged) { - // assumes that the detail values have already been loaded, as the detail type hasn't changed - fixesAndTails.updateDetailValueBoundaries(competitorSelection.getSelectedCompetitors()); - } - // update tails for all competitors because selection change may also affect all unselected competitors - for (CompetitorDTO oneOfAllCompetitors : competitorSelection.getAllCompetitors()) { - Colorline tail = fixesAndTails.getTail(oneOfAllCompetitors); - if (tail != null) { - ColorlineOptions newOptions = createTailStyle(oneOfAllCompetitors, displayHighlighted(oneOfAllCompetitors)); - tail.setOptions(newOptions); // depends on the min/max boundaries computed above - } - } - // Trigger auto-zoom if needed - final RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); - if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { - zoomMapToNewBounds(zoomSettings.getNewBounds(this)); - } - redraw(); + scheduleSelectionDependentTailAndMapStateRefresh(); } @Override @@ -3173,13 +3198,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: - redraw(); + //redraw(); } else { // try a more incremental update otherwise if (settings.isShowOnlySelectedCompetitors()) { // if selection is now empty, show all competitors if (Util.isEmpty(competitorSelection.getSelectedCompetitors())) { - redraw(); + //redraw(); } else { // otherwise remove only deselected competitor's boat images and tail final BoatOverlay removedBoatOverlay = boatOverlaysByCompetitorIdsAsStrings.remove(competitor.getIdAsString()); @@ -3200,21 +3225,7 @@ public class RaceMap extends AbstractCompositeComponent impleme } } // Now update tails for all competitors because selection change may also affect all unselected competitors - if (selectedDetailType != null && !selectedDetailTypeChanged) { - fixesAndTails.updateDetailValueBoundaries(competitorSelection.getSelectedCompetitors()); - } - for (CompetitorDTO oneOfAllCompetitors : competitorSelection.getAllCompetitors()) { - Colorline tail = fixesAndTails.getTail(oneOfAllCompetitors); - if (tail != null) { - ColorlineOptions newOptions = createTailStyle(oneOfAllCompetitors, displayHighlighted(oneOfAllCompetitors)); - tail.setOptions(newOptions); - } - } - // Trigger auto-zoom if needed - RaceMapZoomSettings zoomSettings = settings.getZoomSettings(); - if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { - zoomMapToNewBounds(zoomSettings.getNewBounds(this)); - } + scheduleSelectionDependentTailAndMapStateRefresh(); } private boolean isShowAnyHelperLines() { From 1d4873651b4c7249188bfc6630f66cd971985a67 Mon Sep 17 00:00:00 2001 From: "masha.kashirina" Date: Wed, 18 Mar 2026 19:58:54 +0100 Subject: [PATCH 02/10] bug6214: fixed raceMap update runtime --- .../gwt/ui/client/shared/racemap/RaceMap.java | 101 ++++++++++++++---- 1 file changed, 82 insertions(+), 19 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 77cc052e2a3..74fa5e9bd8b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -283,6 +283,26 @@ public class RaceMap extends AbstractCompositeComponent impleme * The leeward of two Polylines representing a triangle between startline and first mark. */ private Polyline leewardStartLineMarkToFirstMarkLine; + + /** + * True once a selection-dependent tail/map refresh has already been scheduled for the current + * event cycle. This is used to coalesce many add/remove selection callbacks into one final refresh + * instead of running the expensive refresh logic once per competitor. + */ + private boolean selectionDependentRefreshScheduled; + + /** + * True when at least one selection change occurred and a selection-dependent refresh is still + * needed. The scheduled command clears this flag after performing the consolidated refresh. + */ + private boolean selectionDependentRefreshPending; + + /** + * Remembers the last tail display mode applied per competitor id. This allows us to skip + * reapplying tail styles for competitors whose visual tail state did not change between two + * refreshes. The optimization is especially useful when no detail-based tail coloring is active. + */ + private final Map lastAppliedTailDisplayModes = new HashMap<>(); private class AdvantageLineMouseOverMapHandler implements MouseOverMapHandler { private double trueWindAngle; @@ -3121,12 +3141,23 @@ public class RaceMap extends AbstractCompositeComponent impleme public RaceMapSettings getSettings() { return settings; } - private boolean selectionDependentRefreshScheduled; - private boolean selectionDependentRefreshPending; + /** + * Schedules a single consolidated refresh of selection-dependent tail and map state. + * + *

Selection changes are delivered per competitor, so bulk selection can trigger many + * added/removed callbacks in the same event cycle. Running the expensive tail/map refresh + * immediately in every callback caused repeated redraw and restyling work. This method batches + * those callbacks by marking a refresh as pending and scheduling exactly one final refresh via + * {@link Scheduler#scheduleFinally(Scheduler.ScheduledCommand)}.

+ * + *

If multiple selection events arrive before the scheduled command runs, they only set the + * pending flag again; no additional scheduled refresh is created. When the scheduled command + * executes, it performs one call to {@code refreshSelectionDependentTailAndMapState()} if a + * refresh is still pending.

+ */ private void scheduleSelectionDependentTailAndMapStateRefresh() { selectionDependentRefreshPending = true; - if (!selectionDependentRefreshScheduled) { selectionDependentRefreshScheduled = true; Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { @@ -3151,9 +3182,17 @@ public class RaceMap extends AbstractCompositeComponent impleme for (CompetitorDTO oneOfAllCompetitors : competitorSelection.getAllCompetitors()) { Colorline tail = fixesAndTails.getTail(oneOfAllCompetitors); if (tail != null) { - ColorlineOptions newOptions = - createTailStyle(oneOfAllCompetitors, displayHighlighted(oneOfAllCompetitors)); - tail.setOptions(newOptions); // depends on the min/max boundaries computed above + final DisplayMode newDisplayMode = displayHighlighted(oneOfAllCompetitors); + if (selectedDetailType == null) { + final String competitorId = oneOfAllCompetitors.getIdAsString(); + final DisplayMode previousDisplayMode = lastAppliedTailDisplayModes.get(competitorId); + if (previousDisplayMode == newDisplayMode) { + continue; + } + lastAppliedTailDisplayModes.put(competitorId, newDisplayMode); + } + ColorlineOptions newOptions = createTailStyle(oneOfAllCompetitors, newDisplayMode); + tail.setOptions(newOptions); } } // Trigger auto-zoom if needed @@ -3176,6 +3215,7 @@ public class RaceMap extends AbstractCompositeComponent impleme CanvasOverlayV3 boatOverlay = next.getValue(); boatOverlay.removeFromMap(); fixesAndTails.removeTail(next.getKey()); + lastAppliedTailDisplayModes.remove(next.getKey()); i.remove(); // only this way a ConcurrentModificationException while looping can be avoided } } @@ -3212,6 +3252,7 @@ public class RaceMap extends AbstractCompositeComponent impleme removedBoatOverlay.removeFromMap(); } fixesAndTails.removeTail(competitor.getIdAsString()); + lastAppliedTailDisplayModes.remove(competitor.getIdAsString()); showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); } } else { @@ -3558,7 +3599,27 @@ public class RaceMap extends AbstractCompositeComponent impleme } } } - + /** + * Creates the visual style for one competitor tail based on the current display mode. + * + *

The three display modes are:

+ *
    + *
  • DEFAULT: normal competitor-colored tail with standard stroke width
  • + *
  • SELECTED: highlighted tail with thicker stroke
  • + *
  • NOT_SELECTED: lowlighted grey tail for competitors that are currently not selected
  • + *
+ * + *

A selected tail uses {@link ColorlineMode#POLYCHROMATIC} only when detail-based coloring is + * actually active and detail values are available. Otherwise it stays + * {@link ColorlineMode#MONOCHROMATIC}. This avoids unnecessary switching between monochromatic and + * polychromatic rendering, which would force {@code Colorline.setOptions(...)} to rebuild the tail + * polyline structure via {@code setPath(...)} and was identified as a significant performance cost + * during selection updates.

+ * + * @param competitor the competitor whose tail is being styled + * @param displayMode the desired display mode for this competitor tail + * @return the configured {@link ColorlineOptions} for the tail + */ @Override public ColorlineOptions createTailStyle(CompetitorDTO competitor, DisplayMode displayMode) { final ColorlineOptions options = new ColorlineOptions(); @@ -3572,19 +3633,21 @@ public class RaceMap extends AbstractCompositeComponent impleme options.setStrokeWeight(1); break; case SELECTED: - options.setColorMode(ColorlineMode.POLYCHROMATIC); - options.setColorProvider(fixIndexInTail -> { - final String resultColor; - final Double detailValue; - // If a DetailType has been selected and we are not currently waiting for the first update with the new values - if (selectedDetailType != null && !selectedDetailTypeChanged && (detailValue = fixesAndTails.getDetailValueAt(competitor, fixIndexInTail)) != null) { - resultColor = tailColorMapper.getColor(detailValue); - } else { - resultColor = competitorSelection.getColor(competitor, raceIdentifier).getAsHtml(); - } - return resultColor; - }); options.setStrokeWeight(2); + if (selectedDetailType != null && !selectedDetailTypeChanged) { + options.setColorMode(ColorlineMode.POLYCHROMATIC); + options.setColorProvider(fixIndexInTail -> { + final Double detailValue = fixesAndTails.getDetailValueAt(competitor, fixIndexInTail); + if (detailValue != null) { + return tailColorMapper.getColor(detailValue); + } + return competitorSelection.getColor(competitor, raceIdentifier).getAsHtml(); + }); + } else { + options.setColorMode(ColorlineMode.MONOCHROMATIC); + final String selectedColor = competitorSelection.getColor(competitor, raceIdentifier).getAsHtml(); + options.setColorProvider(fixIndexInTail -> selectedColor); + } break; case NOT_SELECTED: options.setColorMode(ColorlineMode.MONOCHROMATIC); From 85e17dccd8bb9709379fff0e95049ffe5bf273a4 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 19 Mar 2026 11:27:39 +0100 Subject: [PATCH 03/10] bug6214: always suppress tail style update when display mode unchanged --- .../shared/racemap/ColorlineOptions.java | 46 +++++++++++++------ .../gwt/ui/client/shared/racemap/RaceMap.java | 32 ++++++------- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineOptions.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineOptions.java index 6bea1a7138f..fcd0fc013fe 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineOptions.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ColorlineOptions.java @@ -5,24 +5,27 @@ import com.google.gwt.maps.client.overlays.PolylineOptions; public class ColorlineOptions { private ColorlineMode colorMode = ColorlineMode.MONOCHROMATIC; - + private boolean clickable = true; private boolean draggable = false; private boolean editable = false; private boolean geodesic = true; private boolean visible = true; - + private int strokeWeight = 1; private double strokeOpacity = 1.0; - + private int zIndex = 0; - + private ColorlineColorProvider colorProvider; - - public ColorlineOptions() {} + + public ColorlineOptions() { + } + public ColorlineOptions(ColorlineColorProvider colorProvider) { this.colorProvider = colorProvider; } + public ColorlineOptions(PolylineOptions options, ColorlineColorProvider colorProvider) { clickable = options.getClickable(); geodesic = options.getGeodesic(); @@ -32,6 +35,7 @@ public class ColorlineOptions { zIndex = options.getZindex(); this.colorProvider = colorProvider; } + public ColorlineOptions(ColorlineOptions options) { colorMode = options.getColorMode(); colorProvider = options.getColorProvider(); @@ -44,21 +48,20 @@ public class ColorlineOptions { strokeOpacity = options.getStrokeOpacity(); zIndex = options.getZIndex(); } - - + public Polyline newPolylineInstance(int fixIndexInTail) { if (colorProvider == null) { throw new IllegalStateException("A ColorProvider must be set prior to creating new Polylines."); } return newPolylineInstance(colorProvider.getColor(fixIndexInTail)); } - + public Polyline newPolylineInstance(String strokeColor) { Polyline line = Polyline.newInstance(newPolylineOptionsInstance(strokeColor)); line.setEditable(editable); return line; } - + public PolylineOptions newPolylineOptionsInstance(String strokeColor) { PolylineOptions opt = PolylineOptions.newInstance(); opt.setStrokeColor(strokeColor); @@ -70,66 +73,83 @@ public class ColorlineOptions { opt.setZindex(zIndex); return opt; } - + public ColorlineMode getColorMode() { return colorMode; } + public void setColorMode(ColorlineMode colorMode) { this.colorMode = colorMode; } - + public ColorlineColorProvider getColorProvider() { return colorProvider; } + public void setColorProvider(ColorlineColorProvider colorProvider) { this.colorProvider = colorProvider; } - + public boolean getClickable() { return clickable; } + public void setClickable(boolean clickable) { this.clickable = clickable; } + public boolean getDraggable() { return draggable; } + public void setDraggable(boolean draggable) { this.draggable = draggable; } + public boolean getEditable() { return editable; } + public void setEditable(boolean editable) { this.editable = editable; } + public boolean getGeodesic() { return geodesic; } + public void setGeodesic(boolean geodesic) { this.geodesic = geodesic; } + public boolean getVisible() { return visible; } + public void setVisible(boolean visible) { this.visible = visible; } + public int getZIndex() { return zIndex; } + public void setZIndex(int zIndex) { this.zIndex = zIndex; } + public int getStrokeWeight() { return strokeWeight; } + public void setStrokeWeight(int strokeWeight) { this.strokeWeight = strokeWeight; } + public double getStrokeOpacity() { return strokeOpacity; } + public void setStrokeOpacity(double strokeOpacity) { this.strokeOpacity = strokeOpacity; } 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 e225985a86a..bb529bb3f9e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -2658,22 +2658,22 @@ public class RaceMap extends AbstractCompositeComponent impleme } private DisplayMode displayHighlighted(CompetitorDTO competitorDTO) { - boolean competitorisSelected = competitorSelection.isSelected(competitorDTO); + final DisplayMode result; + final boolean competitorisSelected = competitorSelection.isSelected(competitorDTO); if (!settings.isShowOnlySelectedCompetitors()) { if (competitorisSelected) { - return DisplayMode.SELECTED; + result = DisplayMode.SELECTED; } else { if (isSomeOtherCompetitorSelected()) { - return DisplayMode.NOT_SELECTED; + result = DisplayMode.NOT_SELECTED; } else { - return DisplayMode.DEFAULT; + result = DisplayMode.DEFAULT; } } + } else { + result = competitorSelection.isSelected(competitorDTO) ? DisplayMode.SELECTED : DisplayMode.DEFAULT; } - else{ - return competitorSelection.isSelected(competitorDTO) ? DisplayMode.SELECTED : DisplayMode.DEFAULT; - } - + return result; } private boolean isSomeOtherCompetitorSelected(){ @@ -3163,7 +3163,6 @@ public class RaceMap extends AbstractCompositeComponent impleme @Override public void execute() { selectionDependentRefreshScheduled = false; - if (selectionDependentRefreshPending) { selectionDependentRefreshPending = false; refreshSelectionDependentTailAndMapState(); @@ -3172,6 +3171,7 @@ public class RaceMap extends AbstractCompositeComponent impleme }); } } + private void refreshSelectionDependentTailAndMapState() { if (selectedDetailType != null && !selectedDetailTypeChanged) { // assumes that the detail values have already been loaded, as the detail type hasn't changed @@ -3182,16 +3182,12 @@ public class RaceMap extends AbstractCompositeComponent impleme Colorline tail = fixesAndTails.getTail(oneOfAllCompetitors); if (tail != null) { final DisplayMode newDisplayMode = displayHighlighted(oneOfAllCompetitors); - if (selectedDetailType == null) { - final String competitorId = oneOfAllCompetitors.getIdAsString(); - final DisplayMode previousDisplayMode = lastAppliedTailDisplayModes.get(competitorId); - if (previousDisplayMode == newDisplayMode) { - continue; - } + final String competitorId = oneOfAllCompetitors.getIdAsString(); + if (lastAppliedTailDisplayModes.get(competitorId) != newDisplayMode) { lastAppliedTailDisplayModes.put(competitorId, newDisplayMode); + ColorlineOptions newOptions = createTailStyle(oneOfAllCompetitors, newDisplayMode); + tail.setOptions(newOptions); } - ColorlineOptions newOptions = createTailStyle(oneOfAllCompetitors, newDisplayMode); - tail.setOptions(newOptions); } } // Trigger auto-zoom if needed @@ -3613,7 +3609,7 @@ public class RaceMap extends AbstractCompositeComponent impleme * {@link ColorlineMode#MONOCHROMATIC}. This avoids unnecessary switching between monochromatic and * polychromatic rendering, which would force {@code Colorline.setOptions(...)} to rebuild the tail * polyline structure via {@code setPath(...)} and was identified as a significant performance cost - * during selection updates.

+ * during selection updates. See also bug 6214.

* * @param competitor the competitor whose tail is being styled * @param displayMode the desired display mode for this competitor tail From a1b20fe18ebdcae4dee99287602147e8aae8218c Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 19 Mar 2026 11:35:16 +0100 Subject: [PATCH 04/10] bug6214: moving tail removal into a method to stay consistent --- .../gwt/ui/client/shared/racemap/RaceMap.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 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 bb529bb3f9e..5f7f89aca9f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -2026,12 +2026,17 @@ public class RaceMap extends AbstractCompositeComponent impleme boatCanvas.removeFromMap(); boatOverlaysByCompetitorIdsAsStrings.remove(unusedBoatCanvasCompetitorDTO); } - for (String unusedTailCompetitorDTO : competitorIdsAsStringOfUnusedTails) { - fixesAndTails.removeTail(unusedTailCompetitorDTO); + for (String unusedTailCompetitorIdAsString : competitorIdsAsStringOfUnusedTails) { + removeTail(unusedTailCompetitorIdAsString); } } } } + + private void removeTail(String competitorIdAsString) { + fixesAndTails.removeTail(competitorIdAsString); + lastAppliedTailDisplayModes.remove(competitorIdAsString); + } /** * This algorithm is limited to distances such that dlon < pi/2, i.e., those that extend around less than one @@ -3209,8 +3214,7 @@ public class RaceMap extends AbstractCompositeComponent impleme if (!next.getKey().equals(competitor.getIdAsString())) { CanvasOverlayV3 boatOverlay = next.getValue(); boatOverlay.removeFromMap(); - fixesAndTails.removeTail(next.getKey()); - lastAppliedTailDisplayModes.remove(next.getKey()); + removeTail(next.getKey()); i.remove(); // only this way a ConcurrentModificationException while looping can be avoided } } @@ -3246,8 +3250,7 @@ public class RaceMap extends AbstractCompositeComponent impleme if (removedBoatOverlay != null) { removedBoatOverlay.removeFromMap(); } - fixesAndTails.removeTail(competitor.getIdAsString()); - lastAppliedTailDisplayModes.remove(competitor.getIdAsString()); + removeTail(competitor.getIdAsString()); showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); } } else { From ea493ec86db2a64591e1130a6f542fe58ebfbcde Mon Sep 17 00:00:00 2001 From: "masha.kashirina" Date: Fri, 20 Mar 2026 19:27:30 +0100 Subject: [PATCH 05/10] bug6214: redraw() is called only when needed --- .../gwt/ui/client/shared/racemap/RaceMap.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 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 5f7f89aca9f..8b983d121df 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMap.java @@ -290,12 +290,13 @@ public class RaceMap extends AbstractCompositeComponent impleme * instead of running the expensive refresh logic once per competitor. */ private boolean selectionDependentRefreshScheduled; - + /** - * True when at least one selection change occurred and a selection-dependent refresh is still - * needed. The scheduled command clears this flag after performing the consolidated refresh. + * True once a selection-dependent tail/map refresh needs redraw. + * For example, we avoid redraw in this situation: + * no helper lines, all competitors shown, one is getting deselected. */ - private boolean selectionDependentRefreshPending; + private boolean selectionDependentRefreshNeedsRedraw; /** * Remembers the last tail display mode applied per competitor id. This allows us to skip @@ -3161,17 +3162,13 @@ public class RaceMap extends AbstractCompositeComponent impleme * refresh is still pending.

*/ private void scheduleSelectionDependentTailAndMapStateRefresh() { - selectionDependentRefreshPending = true; if (!selectionDependentRefreshScheduled) { selectionDependentRefreshScheduled = true; Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { @Override public void execute() { selectionDependentRefreshScheduled = false; - if (selectionDependentRefreshPending) { - selectionDependentRefreshPending = false; - refreshSelectionDependentTailAndMapState(); - } + refreshSelectionDependentTailAndMapState(); } }); } @@ -3200,7 +3197,9 @@ public class RaceMap extends AbstractCompositeComponent impleme if (!zoomSettings.containsZoomType(ZoomTypes.NONE) && zoomSettings.isZoomToSelectedCompetitors()) { zoomMapToNewBounds(zoomSettings.getNewBounds(this)); } - redraw(); + if (selectionDependentRefreshNeedsRedraw) { + redraw(); + } } @Override @@ -3237,13 +3236,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: - //redraw(); + selectionDependentRefreshNeedsRedraw = true; } else { // try a more incremental update otherwise if (settings.isShowOnlySelectedCompetitors()) { // if selection is now empty, show all competitors if (Util.isEmpty(competitorSelection.getSelectedCompetitors())) { - //redraw(); + selectionDependentRefreshNeedsRedraw = true; } else { // otherwise remove only deselected competitor's boat images and tail final BoatOverlay removedBoatOverlay = boatOverlaysByCompetitorIdsAsStrings.remove(competitor.getIdAsString()); @@ -3252,6 +3251,7 @@ public class RaceMap extends AbstractCompositeComponent impleme } removeTail(competitor.getIdAsString()); showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); + selectionDependentRefreshNeedsRedraw = false; } } else { // "lowlight" currently selected competitor @@ -3261,6 +3261,7 @@ public class RaceMap extends AbstractCompositeComponent impleme boatCanvas.draw(); } showCompetitorInfoOnMap(timer.getTime(), -1, competitorSelection.getSelectedFilteredCompetitors()); + selectionDependentRefreshNeedsRedraw = false; } } // Now update tails for all competitors because selection change may also affect all unselected competitors From 1c3edb89bd08adab3389f4b949beab6a2ba5ae2f Mon Sep 17 00:00:00 2001 From: "masha.kashirina" Date: Sun, 22 Mar 2026 17:40:21 +0100 Subject: [PATCH 06/10] bug6214: choose RefreshableMultiSelectionModel to be the one that is handling the table selection and the entity comparison, remove redunant listener/notifier structure --- .../gwt/ui/leaderboard/LeaderboardPanel.java | 82 ++----------------- 1 file changed, 5 insertions(+), 77 deletions(-) diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java index 3676f7acf92..9239d77cd4d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java @@ -29,7 +29,6 @@ import com.google.gwt.dom.client.Style.FontWeight; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; -import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.i18n.client.NumberFormat; import com.google.gwt.resources.client.ImageResource; import com.google.gwt.safecss.shared.SafeStyles; @@ -331,14 +330,6 @@ public abstract class LeaderboardPanel extends A */ private final Handler selectionChangeHandler; - /** - * While the {@link #selectionChangeHandler} is registered as a selection change handler on the - * {@link #leaderboardTable}'s selection model, this field holds the registration which can be used to remove the - * registration again. We'll use this to temporarily suspend selection events when actively modifying / adjusting - * the table selection to match the {@link #competitorSelectionProvider}. - */ - private HandlerRegistration leaderboardAsTableSelectionModelRegistration; - private final FlowPanel contentPanel = new FlowPanel(); protected final PaywallResolver paywallResolver; @@ -524,7 +515,7 @@ public abstract class LeaderboardPanel extends A leaderboardTable.ensureDebugId("LeaderboardCellTable"); selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider); leaderboardTable.setWidth("100%"); - leaderboardSelectionModel = new MultiSelectionModel(); + leaderboardSelectionModel = selectionCheckboxColumn.getSelectionModel(); // remember handler registration so we can temporarily remove it and re-add it to suspend selection events while // we're actively changing it selectionChangeHandler = new Handler() { @@ -542,8 +533,7 @@ public abstract class LeaderboardPanel extends A } } }; - leaderboardAsTableSelectionModelRegistration = leaderboardSelectionModel - .addSelectionChangeHandler(selectionChangeHandler); + leaderboardSelectionModel.addSelectionChangeHandler(selectionChangeHandler); leaderboardTable.setSelectionModel(leaderboardSelectionModel, selectionCheckboxColumn.getSelectionManager()); SimplePanel mainPanel = new SimplePanel(); @@ -2115,8 +2105,7 @@ public abstract class LeaderboardPanel extends A } private class LeaderboardSelectionCheckboxColumn - extends com.sap.sse.gwt.client.celltable.SelectionCheckboxColumn - implements CompetitorSelectionChangeListener { + extends com.sap.sse.gwt.client.celltable.SelectionCheckboxColumn { protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) { super(style.getTableresources().cellTableStyle().cellTableCheckboxSelected(), style.getTableresources().cellTableStyle().cellTableCheckboxDeselected(), @@ -2132,49 +2121,13 @@ public abstract class LeaderboardPanel extends A return t.competitor.getIdAsString().hashCode(); } }, getData(), leaderboardTable); - competitorSelectionProvider.addCompetitorSelectionChangeListener(this); } @Override public Boolean getValue(LeaderboardRowDTO row) { return competitorSelectionProvider.isSelected(row.competitor); } - - @Override - public void competitorsListChanged(Iterable competitors) { - } - - @Override - public void filterChanged(FilterSet> oldFilterSet, - FilterSet> newFilterSet) { - } - - @Override - public void filteredCompetitorsListChanged(Iterable filteredCompetitors) { - } - - /** - * Ensure that the checkbox is redrawn when the competitor selection changes - */ - @Override - public void addedToSelection(CompetitorDTO competitor) { - final LeaderboardRowDTO row = getRow(competitor.getIdAsString()); - if (row != null) { - getSelectionModel().setSelected(row, true); - } - } - - /** - * Ensure that the checkbox is redrawn when the competitor selection changes - */ - @Override - public void removedFromSelection(CompetitorDTO competitor) { - final LeaderboardRowDTO row = getRow(competitor.getIdAsString()); - if (row != null) { - getSelectionModel().setSelected(row, false); - } - } - + @Override public void updateMinMax() { } @@ -2642,15 +2595,10 @@ public abstract class LeaderboardPanel extends A } } for (Entry updateEntry : rowsToUpdate.entrySet()) { - LeaderboardRowDTO oldElement = getData().getList().set(updateEntry.getKey(), - updateEntry.getValue()); - leaderboardSelectionModel.setSelected(oldElement, false); // make sure the old element is no longer - // part of the selection - updateSelection(updateEntry.getValue()); + getData().getList().set(updateEntry.getKey(), updateEntry.getValue()); } for (LeaderboardRowDTO rowToAdd : rowsToAdd) { getData().getList().add(rowToAdd); - updateSelection(rowToAdd); } } RaceColumn lastRaceColumn = null; @@ -2700,26 +2648,6 @@ public abstract class LeaderboardPanel extends A protected abstract void applyTop30FilterIfCompetitorSizeGreaterEqual40(LeaderboardDTO leaderboard); - /** - * Adjusts the row's selection in the {@link #leaderboardSelectionModel} so it matches its selection state in the - * {@link #competitorSelectionProvider}. - */ - private void updateSelection(LeaderboardRowDTO row) { - final boolean shallBeSelected = competitorSelectionProvider.isSelected(row.competitor); - if (leaderboardAsTableSelectionModelRegistration != null) { - // suspend selection events while actively adjusting the leaderboardSelectionModel to match the - // competitorSelectionProvider - leaderboardAsTableSelectionModelRegistration.removeHandler(); - leaderboardAsTableSelectionModelRegistration = null; - } - if (leaderboardSelectionModel.isSelected(row) != shallBeSelected) { - leaderboardSelectionModel.setSelected(row, shallBeSelected); - } - // register the selection change handler again - leaderboardAsTableSelectionModelRegistration = leaderboardTable.getSelectionModel() - .addSelectionChangeHandler(selectionChangeHandler); - } - /** * The race columns hold a now outdated copy of a {@link RaceColumnDTO} which needs to be updated from the * {@link LeaderboardDTO} just received From 1eeacf159b0ad969ac077170070f8568d5180fed Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 25 Mar 2026 22:59:43 +0100 Subject: [PATCH 07/10] bug6214: disabled a SwissTiming competitor import test temporarily --- .../manage2sail/resultimport/test/CompetitorImportTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/CompetitorImportTest.java b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/CompetitorImportTest.java index d46a7868a5d..1ee86747077 100644 --- a/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/CompetitorImportTest.java +++ b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/CompetitorImportTest.java @@ -15,6 +15,7 @@ import java.util.Arrays; import javax.xml.bind.JAXBException; import javax.xml.parsers.ParserConfigurationException; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.xml.sax.SAXException; @@ -40,6 +41,7 @@ import com.sap.sse.common.Util; * */ public class CompetitorImportTest extends AbstractEventResultJsonServiceTest { + @Disabled("ongoing tests at SwissTiming 2026-03-25") @Test public void simpleCompetitorImportTest() throws FileNotFoundException, IOException, JAXBException, URISyntaxException, SAXException, ParserConfigurationException { ResultUrlRegistry resultUrlRegistry = mock(ResultUrlRegistry.class); From 883e96612e1b1da5c4866119ef021de8edd630ee Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 25 Mar 2026 23:05:21 +0100 Subject: [PATCH 08/10] bug6214: revert disabling of SwissTiming test --- .../manage2sail/resultimport/test/CompetitorImportTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/CompetitorImportTest.java b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/CompetitorImportTest.java index 1ee86747077..d46a7868a5d 100644 --- a/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/CompetitorImportTest.java +++ b/java/com.sap.sailing.manage2sail.resultimport.test/src/com/sap/sailing/manage2sail/resultimport/test/CompetitorImportTest.java @@ -15,7 +15,6 @@ import java.util.Arrays; import javax.xml.bind.JAXBException; import javax.xml.parsers.ParserConfigurationException; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.xml.sax.SAXException; @@ -41,7 +40,6 @@ import com.sap.sse.common.Util; * */ public class CompetitorImportTest extends AbstractEventResultJsonServiceTest { - @Disabled("ongoing tests at SwissTiming 2026-03-25") @Test public void simpleCompetitorImportTest() throws FileNotFoundException, IOException, JAXBException, URISyntaxException, SAXException, ParserConfigurationException { ResultUrlRegistry resultUrlRegistry = mock(ResultUrlRegistry.class); From bc3b08f513003166be0e1a5b93a50251e68f1db4 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Wed, 25 Mar 2026 23:02:55 +0100 Subject: [PATCH 09/10] bug6214: don't exclude all tests from com.sap.sailing.landscape.test --- java/com.sap.sailing.landscape.test/pom.xml | 32 --------------------- 1 file changed, 32 deletions(-) diff --git a/java/com.sap.sailing.landscape.test/pom.xml b/java/com.sap.sailing.landscape.test/pom.xml index d07ebaac1bd..1b5838c3fc2 100755 --- a/java/com.sap.sailing.landscape.test/pom.xml +++ b/java/com.sap.sailing.landscape.test/pom.xml @@ -8,37 +8,5 @@ 1.0.0-SNAPSHOT com.sap.sailing.landscape.test - - eclipse-plugin - - true - - - - - org.apache.maven.plugins - maven-surefire-plugin - - - **/*.java - - - - - org.eclipse.tycho - tycho-surefire-plugin - ${tycho-version} - - - **/* - - - - - From aeadcc57d3d32060143aed91add10fd010be8dfc Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 26 Mar 2026 09:41:59 +0100 Subject: [PATCH 10/10] bug6214: disable landscape tests requiring tokens --- .../src/com/sap/sailing/landscape/test/TestProcedures.java | 2 ++ .../com/sap/sailing/landscape/test/TestReleaseRepository.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestProcedures.java b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestProcedures.java index 17fe84cb646..e0d7b2e0edd 100755 --- a/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestProcedures.java +++ b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestProcedures.java @@ -19,6 +19,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import com.jcraft.jsch.JSchException; @@ -76,6 +77,7 @@ import software.amazon.awssdk.services.route53.model.RRType; * @author Axel Uhl (D043530) * */ +@Disabled("This requires an AWS token in the launch configuration") public class TestProcedures { private static final Logger logger = Logger.getLogger(TestProcedures.class.getName()); private static final Optional optionalTimeout = Optional.of(Duration.ONE_MINUTE.times(10)); diff --git a/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestReleaseRepository.java b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestReleaseRepository.java index 38fab1e16be..1e7704c137b 100755 --- a/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestReleaseRepository.java +++ b/java/com.sap.sailing.landscape.test/src/com/sap/sailing/landscape/test/TestReleaseRepository.java @@ -3,11 +3,13 @@ package com.sap.sailing.landscape.test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import com.sap.sailing.landscape.SailingReleaseRepository; import com.sap.sse.common.Util; +@Disabled("Don't go against our Github rate limit") public class TestReleaseRepository { @Test public void testForAtLeastOneRelease() {