diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiDevicesPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiDevicesPanel.java index d6b92bcfd6a..1856d18e5cd 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiDevicesPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiDevicesPanel.java @@ -33,7 +33,6 @@ import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CaptionPanel; -import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.DialogBox; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Focusable; @@ -297,12 +296,6 @@ public class IgtimiDevicesPanel extends FlowPanel implements FilterablePanelProv dialogContent.setWidth("100%"); final HorizontalPanel dialogHeader = new HorizontalPanel(); dialogHeader.setWidth("100%"); - final CheckBox orientationToggle = new CheckBox(stringMessages.windChartVertical()); - orientationToggle.addValueChangeHandler(event -> { - windChartPanel.toggleOrientation(); - windChartPanel.resizeChart(resizableContainer.getOffsetWidth(), resizableContainer.getOffsetHeight()); - }); - dialogHeader.add(orientationToggle); dialogHeader.setHorizontalAlignment(HorizontalPanel.ALIGN_RIGHT); final Button closeButton = new Button("✕"); closeButton.addClickHandler(event -> windChartDialog.hide()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindLiveChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindLiveChart.java index 9a7c2c30f46..2c6d8e2d8f8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindLiveChart.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindLiveChart.java @@ -16,10 +16,14 @@ import org.moxieapps.gwt.highcharts.client.labels.YAxisLabels; import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEvent; +import org.moxieapps.gwt.highcharts.client.events.ChartSelectionEventHandler; + import com.google.gwt.i18n.client.DateTimeFormat; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.RequiresResize; import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.impl.ColorMapImpl; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.charts.ChartUtil; import com.sap.sailing.gwt.ui.client.shared.charts.WindChartDataRenderer; @@ -29,12 +33,18 @@ public class WindLiveChart extends Composite implements RequiresResize { private final Chart chart; private final WindChartDataRenderer windChartDataRenderer; private final boolean inverted; + private boolean userZoomed = false; public WindLiveChart(final StringMessages stringMessages) { - this(stringMessages, /* inverted */ false); + this(stringMessages, /* inverted */ false, new ColorMapImpl<>()); } public WindLiveChart(final StringMessages stringMessages, final boolean inverted) { + this(stringMessages, inverted, new ColorMapImpl<>()); + } + + public WindLiveChart(final StringMessages stringMessages, final boolean inverted, + final ColorMapImpl colorMap) { this.inverted = inverted; chart = new Chart() .setWidth100() @@ -44,7 +54,14 @@ public class WindLiveChart extends Composite implements RequiresResize { .setChartTitle(new ChartTitle().setText("")) .setCredits(new Credits().setEnabled(false)) .setLinePlotOptions(new LinePlotOptions() - .setMarker(new Marker().setEnabled(false))); + .setMarker(new Marker().setEnabled(false))) + .setSelectionEventHandler(new ChartSelectionEventHandler() { + @Override + public boolean onSelection(final ChartSelectionEvent event) { + userZoomed = event.getXAxisMinAsLongOrNull() != null; + return true; + } + }); chart.getXAxis() .setType(Axis.Type.DATE_TIME) .setLabels(new XAxisLabels().setFormatter((AxisLabelsData data) -> @@ -73,11 +90,16 @@ public class WindLiveChart extends Composite implements RequiresResize { public void setSeriesPoints(final Series series, final Point[] points) { series.setPoints(points, /* redraw */ false); } - }); + }, + colorMap); initWidget(chart); ensureDebugId("WindLiveChart"); } + public ColorMapImpl getColorMap() { + return windChartDataRenderer.getColorMap(); + } + public void showData(final WindInfoForRaceDTO result) { windChartDataRenderer.updateChartSeries(result, /* append */ false); chart.redraw(); @@ -103,6 +125,14 @@ public class WindLiveChart extends Composite implements RequiresResize { chart.setSize(widthPx, heightPx, false); } + public void setXAxisExtremes(final long fromMillis, final long toMillis) { + chart.getXAxis().setExtremes(fromMillis, toMillis, /* redraw */ false, /* animation */ false); + } + + public boolean isUserZoomed() { + return userZoomed; + } + public boolean isInverted() { return inverted; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindLiveChartPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindLiveChartPanel.java index 08730338ca5..bf5f118f66e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindLiveChartPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindLiveChartPanel.java @@ -7,11 +7,16 @@ import java.util.HashSet; import java.util.Set; import com.google.gwt.core.client.Scheduler; +import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.rpc.AsyncCallback; -import com.google.gwt.user.client.ui.CaptionPanel; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.CheckBox; +import com.google.gwt.user.client.ui.HorizontalPanel; +import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.RequiresResize; import com.google.gwt.user.client.ui.SimplePanel; +import com.google.gwt.user.client.ui.VerticalPanel; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.gwt.ui.client.SailingServiceWriteAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -20,20 +25,25 @@ import com.sap.sailing.gwt.ui.shared.WindTrackInfoDTO; import com.sap.sse.common.Duration; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.gwt.client.ErrorReporter; +import com.sap.sse.gwt.client.Notification; +import com.sap.sse.gwt.client.Notification.NotificationType; +import com.sap.sse.gwt.client.controls.IntegerBox; +import com.sap.sse.gwt.client.controls.datetime.DateAndTimeInput; +import com.sap.sse.gwt.client.controls.datetime.DateTimeInput.Accuracy; -public class WindLiveChartPanel extends CaptionPanel implements RequiresResize { +public class WindLiveChartPanel extends VerticalPanel implements RequiresResize { @FunctionalInterface public interface WindHistoryLoader { void load(WindSource windSource, Date from, Date to, AsyncCallback callback); } - private static final Duration DEFAULT_WIND_HISTORY_DURATION = Duration.ONE_MINUTE.times(10); + private static final int DEFAULT_DURATION_MINUTES = 5; + private static final int MAX_DURATION_MINUTES = 120; private static final Duration DEFAULT_WIND_UPDATE_INTERVAL = Duration.ONE_SECOND.times(3); private final SailingServiceWriteAsync sailingServiceWrite; private final WindHistoryLoader windHistoryLoader; private final ErrorReporter errorReporter; - private final Duration windHistoryDuration; private final Duration windUpdateInterval; private final StringMessages stringMessages; private final SimplePanel chartContainer; @@ -44,6 +54,14 @@ public class WindLiveChartPanel extends CaptionPanel implements RequiresResize { private int windSelectionVersion; private String windLiveUpdateRequestPendingSubscriptionId; + private final HorizontalPanel header; + private final CheckBox liveCheckBox; + private final IntegerBox durationBox; + private final SimplePanel durationPanel; + private final DateAndTimeInput fromInput; + private final DateAndTimeInput toInput; + private final SimplePanel staticPanel; + private final Timer windLiveUpdateTimer = new Timer() { @Override public void run() { @@ -51,42 +69,140 @@ public class WindLiveChartPanel extends CaptionPanel implements RequiresResize { } }; - /** - * Creates a live wind chart using ten minutes of initial history and a three-second live update interval. - */ public WindLiveChartPanel(final SailingServiceWriteAsync sailingServiceWrite, final WindHistoryLoader windHistoryLoader, final StringMessages stringMessages, final ErrorReporter errorReporter) { this(sailingServiceWrite, windHistoryLoader, stringMessages, errorReporter, - DEFAULT_WIND_HISTORY_DURATION, DEFAULT_WIND_UPDATE_INTERVAL); + DEFAULT_WIND_UPDATE_INTERVAL); } - /** - * Creates a live wind chart with caller-defined initial history and live update intervals. - */ public WindLiveChartPanel(final SailingServiceWriteAsync sailingServiceWrite, final WindHistoryLoader windHistoryLoader, final StringMessages stringMessages, - final ErrorReporter errorReporter, final Duration windHistoryDuration, - final Duration windUpdateInterval) { - //no name because window has the same one already (careful with getChild to avoid null exceptions) - super(""); + final ErrorReporter errorReporter, final Duration windUpdateInterval) { this.sailingServiceWrite = sailingServiceWrite; this.windHistoryLoader = windHistoryLoader; this.errorReporter = errorReporter; - this.windHistoryDuration = windHistoryDuration; this.windUpdateInterval = windUpdateInterval; this.stringMessages = stringMessages; + // controls + header = new HorizontalPanel(); + header.setSpacing(4); + liveCheckBox = new CheckBox(stringMessages.windChartLive()); + liveCheckBox.setValue(true); + header.add(liveCheckBox); + // live mode: duration input + durationBox = new IntegerBox(); + durationBox.setVisibleLength(3); + durationBox.setValue(DEFAULT_DURATION_MINUTES); + final HorizontalPanel durationInner = new HorizontalPanel(); + durationInner.setSpacing(2); + durationInner.add(durationBox); + durationInner.add(new Label(stringMessages.windChartDurationMinutes())); + durationPanel = new SimplePanel(durationInner); + header.add(durationPanel); + // static mode: from/to pickers + show button + fromInput = new DateAndTimeInput(Accuracy.MINUTES); + toInput = new DateAndTimeInput(Accuracy.MINUTES); + final Button showButton = new Button(stringMessages.windChartShow()); + final HorizontalPanel staticInner = new HorizontalPanel(); + staticInner.setSpacing(2); + staticInner.add(new Label(stringMessages.windChartFrom())); + staticInner.add(fromInput); + staticInner.add(new Label(stringMessages.windChartTo())); + staticInner.add(toInput); + staticInner.add(showButton); + staticPanel = new SimplePanel(staticInner); + staticPanel.setVisible(false); + header.add(staticPanel); + // orientation toggle + final CheckBox orientationToggle = new CheckBox(stringMessages.windChartVertical()); + orientationToggle.addValueChangeHandler(event -> toggleOrientation()); + header.add(orientationToggle); + add(header); + // chart windChart = new WindLiveChart(stringMessages); chartContainer = new SimplePanel(windChart); chartContainer.setWidth("100%"); chartContainer.setHeight("100%"); add(chartContainer); - setVisible(false); + // wire controls + liveCheckBox.addValueChangeHandler(this::onLiveToggled); + durationBox.addValueChangeHandler(event -> onSettingsChanged()); + showButton.addClickHandler(event -> onShowClicked()); } + /** + * Handles the Live checkbox toggle. + *

+ * Live → static: stops the subscription, freezes the current [now-duration, now] window into + * the from/to pickers and locks the x-axis there. The already-displayed data stays visible — + * no re-fetch. + *

+ * Static → live: resets duration to the default and restarts live mode. + */ + private void onLiveToggled(final ValueChangeEvent event) { + final boolean live = Boolean.TRUE.equals(event.getValue()); + durationPanel.setVisible(live); + staticPanel.setVisible(!live); + if (live) { + durationBox.setValue(DEFAULT_DURATION_MINUTES); + onSettingsChanged(); + } else { + // freeze at current live window(stop timer, populate pickers, keep displayed data as-is) + stopWindLiveSubscription(); + final int durationMinutes = clampDuration(durationBox.getValue()); + final Date to = new Date(); + final Date from = new MillisecondsTimePoint(to).minus(Duration.ONE_MINUTE.times(durationMinutes)).asDate(); + fromInput.setValue(from); + toInput.setValue(to); + windChart.setXAxisExtremes(from.getTime(), to.getTime()); + } + } + + /** + * Validates the from/to range and triggers a static history load on success. + * Shows a WARNING notification if to ≤ from or if the range exceeds 2 hours. + */ + private void onShowClicked() { + final Date from = fromInput.getValue(); + final Date to = toInput.getValue(); + if (from == null || to == null) { + return; + } + if (!to.after(from)) { + Notification.notify(stringMessages.windChartToBeforeFrom(), NotificationType.WARNING); + return; + } + final long rangeMillis = to.getTime() - from.getTime(); + final long twoHoursMillis = Duration.ONE_MINUTE.times(MAX_DURATION_MINUTES).asMillis(); + if (rangeMillis > twoHoursMillis) { + Notification.notify(stringMessages.windChartRangeExceeded(), NotificationType.WARNING); + return; + } + if (!shownWindSources.isEmpty()) { + loadStaticHistory(new HashSet<>(shownWindSources), from, to); + } + } + + /** Called when the duration box changes; restarts live mode with the new window size. */ + private void onSettingsChanged() { + final Integer raw = durationBox.getValue(); + if (raw != null && raw > MAX_DURATION_MINUTES) { + Notification.notify(stringMessages.windChartRangeExceeded(), NotificationType.WARNING); + return; + } + if (!shownWindSources.isEmpty()) { + reloadForCurrentSettings(); + } + } + + /** + * Sets the wind sources to display. Hides the panel when the set is empty. + * Switches to live mode unconditionally: any existing subscription is stopped, + * history is loaded for [now-duration, now], and live updates begin. + */ public void setSelectedWindSources(final Set selectedWindSources) { final Set windSources = new HashSet<>(selectedWindSources); - final int selectionVersion = ++windSelectionVersion; stopWindLiveSubscription(); lastHistoryData = null; for (final WindSource windSource : shownWindSources) { @@ -95,82 +211,80 @@ public class WindLiveChartPanel extends CaptionPanel implements RequiresResize { shownWindSources.clear(); setVisible(!windSources.isEmpty()); if (!windSources.isEmpty()) { + shownWindSources.addAll(windSources); Scheduler.get().scheduleDeferred(windChart::onResize); - sailingServiceWrite.startWindLiveSubscription( - windSources, /* correctByDeclination */ true, - new AsyncCallback() { - @Override - public void onSuccess(final String subscriptionId) { - if (selectionVersion != windSelectionVersion) { - stopWindLiveSubscription(subscriptionId); - } else { - windLiveSubscriptionId = subscriptionId; - shownWindSources.addAll(windSources); - loadWindHistory(windSources, new Date(), selectionVersion); - } - } - - @Override - public void onFailure(final Throwable caught) { - if (selectionVersion == windSelectionVersion) { - setVisible(false); - errorReporter.reportError(caught.getMessage()); - } - } - }); + reloadForCurrentSettings(); } } - private void loadWindHistory(final Set windSources, final Date to, final int selectionVersion) { - final Date from = new MillisecondsTimePoint(to).minus(windHistoryDuration).asDate(); - final Set windSourcesPending = new HashSet<>(windSources); - final WindInfoForRaceDTO combinedHistory = new WindInfoForRaceDTO(); - combinedHistory.windTrackInfoByWindSource = new HashMap<>(); - for (final WindSource windSource : windSources) { - windHistoryLoader.load( - windSource, - from, - to, - new AsyncCallback() { - @Override - public void onSuccess(final WindInfoForRaceDTO result) { - if (selectionVersion == windSelectionVersion) { - if (result != null && result.windTrackInfoByWindSource != null) { - combinedHistory.windTrackInfoByWindSource.putAll(result.windTrackInfoByWindSource); - } - onWindHistoryRequestCompleted(windSource, windSourcesPending, combinedHistory, - selectionVersion); - } - } - - @Override - public void onFailure(final Throwable caught) { - if (selectionVersion == windSelectionVersion) { - errorReporter.reportError(caught.getMessage()); - onWindHistoryRequestCompleted(windSource, windSourcesPending, combinedHistory, - selectionVersion); - } - } - }); - } + /** Stops any current subscription and restarts live mode for the current duration. */ + private void reloadForCurrentSettings() { + stopWindLiveSubscription(); + lastHistoryData = null; + final int durationMinutes = clampDuration(durationBox.getValue()); + final Duration duration = Duration.ONE_MINUTE.times(durationMinutes); + final Date to = new Date(); + final Date from = new MillisecondsTimePoint(to).minus(duration).asDate(); + startLiveMode(new HashSet<>(shownWindSources), from, to, duration); } - private void onWindHistoryRequestCompleted( - final WindSource windSource, - final Set windSourcesPending, - final WindInfoForRaceDTO combinedHistory, - final int selectionVersion) { - windSourcesPending.remove(windSource); - if (selectionVersion == windSelectionVersion - && windSourcesPending.isEmpty() - && windLiveSubscriptionId != null) { - lastHistoryData = combinedHistory; - windChart.showData(combinedHistory); - loadWindLiveUpdates(); - windLiveUpdateTimer.scheduleRepeating((int) windUpdateInterval.asMillis()); + /** Returns the duration clamped to [1, MAX_DURATION_MINUTES], falling back to the default if null or invalid. */ + private int clampDuration(final Integer raw) { + if (raw == null || raw < 1) { + return DEFAULT_DURATION_MINUTES; } + return Math.min(raw, MAX_DURATION_MINUTES); + } + + /** + * Opens a server-side live subscription, loads history for [from, to] across all wind sources + * in parallel, then — once all responses arrive — shows the combined data, applies the initial + * x-axis window, and starts the periodic update timer. + *

+ * Uses {@code windSelectionVersion} to discard stale callbacks: if the version has advanced by + * the time a response arrives, the result is dropped and any orphaned subscription is stopped. + */ + private void startLiveMode(final Set windSources, final Date from, final Date to, + final Duration duration) { + final int selectionVersion = ++windSelectionVersion; + sailingServiceWrite.startWindLiveSubscription( + windSources, /* correctByDeclination */ true, + new AsyncCallback() { + @Override + public void onSuccess(final String subscriptionId) { + if (selectionVersion != windSelectionVersion) { + stopWindLiveSubscription(subscriptionId); + } else { + windLiveSubscriptionId = subscriptionId; + loadHistory(windSources, from, to, selectionVersion, () -> { + applyLiveExtremes(duration); + loadWindLiveUpdates(); + windLiveUpdateTimer.scheduleRepeating((int) windUpdateInterval.asMillis()); + }); + } + } + + @Override + public void onFailure(final Throwable caught) { + if (selectionVersion == windSelectionVersion) { + errorReporter.reportError(caught.getMessage()); + } + } + }); } + /** Sets the x-axis to [now-duration, now]. Called on each live tick (unless user is zoomed). */ + private void applyLiveExtremes(final Duration duration) { + final long toMillis = new Date().getTime(); + final long fromMillis = toMillis - duration.asMillis(); + windChart.setXAxisExtremes(fromMillis, toMillis); + } + + /** + * Fetches the latest live data points for the current subscription and appends them to the chart. + * Guards against concurrent requests: a second call while one is in flight is a no-op. + * After appending, advances the x-axis sliding window unless the user has dragged to zoom. + */ private void loadWindLiveUpdates() { final String subscriptionId = windLiveSubscriptionId; if (subscriptionId != null @@ -187,6 +301,10 @@ public class WindLiveChartPanel extends CaptionPanel implements RequiresResize { if (subscriptionId.equals(windLiveSubscriptionId)) { mergeIntoLastHistoryData(result); windChart.appendData(result); + if (!windChart.isUserZoomed()) { + final int durationMinutes = clampDuration(durationBox.getValue()); + applyLiveExtremes(Duration.ONE_MINUTE.times(durationMinutes)); + } } } @@ -203,6 +321,68 @@ public class WindLiveChartPanel extends CaptionPanel implements RequiresResize { } } + /** + * Loads history for the given [from, to] range once and locks the x-axis to that window. + * No timer is started; the chart is static until the user clicks Show again. + */ + private void loadStaticHistory(final Set windSources, final Date from, final Date to) { + final int selectionVersion = ++windSelectionVersion; + loadHistory(windSources, from, to, selectionVersion, () -> { + windChart.setXAxisExtremes(from.getTime(), to.getTime()); + }); + } + + /** + * Fires one history request per wind source in parallel and collects the results into a single + * {@link WindInfoForRaceDTO}. Calls {@code onComplete} once all requests have returned (success + * or failure). Stale responses (version mismatch) are silently discarded. + */ + private void loadHistory(final Set windSources, final Date from, final Date to, + final int selectionVersion, final Runnable onComplete) { + final Set pending = new HashSet<>(windSources); + final WindInfoForRaceDTO combined = new WindInfoForRaceDTO(); + combined.windTrackInfoByWindSource = new HashMap<>(); + for (final WindSource windSource : windSources) { + windHistoryLoader.load(windSource, from, to, new AsyncCallback() { + @Override + public void onSuccess(final WindInfoForRaceDTO result) { + if (selectionVersion == windSelectionVersion) { + if (result != null && result.windTrackInfoByWindSource != null) { + combined.windTrackInfoByWindSource.putAll(result.windTrackInfoByWindSource); + } + onHistoryRequestCompleted(windSource, pending, combined, selectionVersion, onComplete); + } + } + @Override + public void onFailure(final Throwable caught) { + if (selectionVersion == windSelectionVersion) { + errorReporter.reportError(caught.getMessage()); + onHistoryRequestCompleted(windSource, pending, combined, selectionVersion, onComplete); + } + } + }); + } + } + + /** + * Called after each individual history request completes. When the last pending source is + * done, renders the combined data and runs {@code onComplete} (e.g. apply x-axis extremes, + * start live timer). + */ + private void onHistoryRequestCompleted(final WindSource windSource, final Set pending, + final WindInfoForRaceDTO combined, final int selectionVersion, final Runnable onComplete) { + pending.remove(windSource); + if (selectionVersion == windSelectionVersion && pending.isEmpty()) { + lastHistoryData = combined; + windChart.showData(combined); + onComplete.run(); + } + } + + /** + * Appends new wind fixes from a live update into {@code lastHistoryData} so that orientation + * toggles and re-renders always have the full accumulated data set available. + */ private void mergeIntoLastHistoryData(final WindInfoForRaceDTO appendedData) { if (appendedData == null || appendedData.windTrackInfoByWindSource == null) { return; @@ -231,7 +411,8 @@ public class WindLiveChartPanel extends CaptionPanel implements RequiresResize { } } } - + + /** Cancels the update timer and fires a server-side stop for the current subscription, if any. */ private void stopWindLiveSubscription() { windLiveUpdateTimer.cancel(); windLiveUpdateRequestPendingSubscriptionId = null; @@ -243,36 +424,50 @@ public class WindLiveChartPanel extends CaptionPanel implements RequiresResize { } private void stopWindLiveSubscription(final String subscriptionId) { - sailingServiceWrite.stopWindLiveSubscription( - subscriptionId, - new AsyncCallback() { - @Override - public void onSuccess(final Void result) { - } + sailingServiceWrite.stopWindLiveSubscription(subscriptionId, new AsyncCallback() { + @Override + public void onSuccess(final Void result) { + } - @Override - public void onFailure(final Throwable caught) { - } - }); + @Override + public void onFailure(final Throwable caught) { + } + }); } - public void toggleOrientation() { + /** + * Replaces the current chart with a new one with the opposite orientation, preserving the + * color map so series keep their assigned colors. Re-renders {@code lastHistoryData} and + * restores the x-axis window (live or static). Resumes the live update timer if a subscription + * is active. + */ + private void toggleOrientation() { windLiveUpdateTimer.cancel(); windLiveUpdateRequestPendingSubscriptionId = null; - final WindLiveChart newChart = new WindLiveChart(stringMessages, !windChart.isInverted()); + final WindLiveChart newChart = new WindLiveChart(stringMessages, !windChart.isInverted(), + windChart.getColorMap()); windChart = newChart; chartContainer.setWidget(newChart); if (lastHistoryData != null) { newChart.showData(lastHistoryData); } if (windLiveSubscriptionId != null) { + final int durationMinutes = clampDuration(durationBox.getValue()); + applyLiveExtremes(Duration.ONE_MINUTE.times(durationMinutes)); loadWindLiveUpdates(); windLiveUpdateTimer.scheduleRepeating((int) windUpdateInterval.asMillis()); + } else { + final Date from = fromInput.getValue(); + final Date to = toInput.getValue(); + if (from != null && to != null) { + newChart.setXAxisExtremes(from.getTime(), to.getTime()); + } } } + /** Resizes the chart, subtracting the header row height so the legend is not clipped. */ public void resizeChart(final int containerWidthPx, final int containerHeightPx) { - windChart.resize(containerWidthPx, containerHeightPx); + windChart.resize(containerWidthPx, containerHeightPx - header.getOffsetHeight()); } @Override 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 71c13b1a201..05ad2e74b14 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 @@ -1220,6 +1220,13 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, String backToLiveTimePanelMode(); String windChart(); String windChartVertical(); + String windChartLive(); + String windChartDurationMinutes(); + String windChartFrom(); + String windChartTo(); + String windChartShow(); + String windChartRangeExceeded(); + String windChartToBeforeFrom(); String windChartLoading(); String mediaNoVideosCaption(); String mediaShowVideoCaption(); 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 d24787f6b6d..470ec8b7be0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -1235,6 +1235,13 @@ raceIsInLiveTimePanelMode=Race is Live backToLiveTimePanelMode=Back to Live windChart=Wind Chart windChartVertical=Vertical +windChartLive=Live +windChartDurationMinutes=min +windChartFrom=From +windChartTo=To +windChartShow=Show +windChartRangeExceeded=Time range exceeds the 2 hour limit. +windChartToBeforeFrom=End time must be after start time. windChartLoading=Loading wind data... mediaNoVideosCaption=No media tracks mediaShowVideoCaption=Show media track 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 e8b2d68c823..3944d3f68c6 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 @@ -1210,6 +1210,13 @@ raceIsInLiveTimePanelMode=Rennen ist live backToLiveTimePanelMode=Zurück zu live windChart=Winddiagramm windChartVertical=Vertikal +windChartLive=Live +windChartDurationMinutes=Min +windChartFrom=Von +windChartTo=Bis +windChartShow=Anzeigen +windChartRangeExceeded=Zeitraum überschreitet das 2-Stunden-Limit. +windChartToBeforeFrom=Endzeit muss nach der Startzeit liegen. windChartLoading=Winddaten werden geladen... mediaNoVideosCaption=Keine Medienspuren mediaShowVideoCaption=Medienspur an diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChartDataRenderer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChartDataRenderer.java index 7382623ae27..d265bb4ccc0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChartDataRenderer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/charts/WindChartDataRenderer.java @@ -43,7 +43,7 @@ public class WindChartDataRenderer { private final Map windSourceSpeedSeries = new HashMap(); private final Map windSourceDirectionPoints = new HashMap(); private final Map windSourceSpeedPoints = new HashMap(); - private final ColorMapImpl colorMap = new ColorMapImpl(); + private final ColorMapImpl colorMap; private Point firstPointOfFirstSeries; private Long timeOfEarliestRequestInMillis; private Long timeOfLatestRequestInMillis; @@ -69,12 +69,26 @@ public class WindChartDataRenderer { private WindChartDataRenderer(final Chart chart, final StringMessages stringMessages, final Integer maxSeriesPoints, final boolean addSeriesToChartOnCreation, final SeriesPointsUpdater seriesPointsUpdater, final PointsAddedHandler pointsAddedHandler) { + this(chart, stringMessages, maxSeriesPoints, addSeriesToChartOnCreation, seriesPointsUpdater, + pointsAddedHandler, new ColorMapImpl<>()); + } + + private WindChartDataRenderer(final Chart chart, final StringMessages stringMessages, final Integer maxSeriesPoints, + final boolean addSeriesToChartOnCreation, final SeriesPointsUpdater seriesPointsUpdater, + final PointsAddedHandler pointsAddedHandler, final ColorMapImpl colorMap) { this.chart = chart; this.stringMessages = stringMessages; this.maxSeriesPoints = maxSeriesPoints; this.addSeriesToChartOnCreation = addSeriesToChartOnCreation; this.seriesPointsUpdater = seriesPointsUpdater; this.pointsAddedHandler = pointsAddedHandler; + this.colorMap = colorMap; + } + + public WindChartDataRenderer(final Chart chart, final StringMessages stringMessages, + final boolean addSeriesToChartOnCreation, final SeriesPointsUpdater seriesPointsUpdater, + final ColorMapImpl colorMap) { + this(chart, stringMessages, null, addSeriesToChartOnCreation, seriesPointsUpdater, null, colorMap); } /**