mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-10-30 15:57:11 +00:00
bug6150: control of the time shown on the chart
This commit is contained in:
1 parent
999be66b10
commit
7df43e4e4a
7 files changed
+363
-110
No files matched your search
-7
@@ -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());
|
||||
|
||||
+33
-3
@@ -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<WindSource> 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<WindSource> 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;
|
||||
}
|
||||
|
||||
+294
-99
@@ -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<WindInfoForRaceDTO> 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* Static → live: resets duration to the default and restarts live mode.
|
||||
*/
|
||||
private void onLiveToggled(final ValueChangeEvent<Boolean> 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<WindSource> selectedWindSources) {
|
||||
final Set<WindSource> 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<String>() {
|
||||
@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<WindSource> windSources, final Date to, final int selectionVersion) {
|
||||
final Date from = new MillisecondsTimePoint(to).minus(windHistoryDuration).asDate();
|
||||
final Set<WindSource> 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<WindInfoForRaceDTO>() {
|
||||
@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<WindSource> 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.
|
||||
* <p>
|
||||
* 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<WindSource> windSources, final Date from, final Date to,
|
||||
final Duration duration) {
|
||||
final int selectionVersion = ++windSelectionVersion;
|
||||
sailingServiceWrite.startWindLiveSubscription(
|
||||
windSources, /* correctByDeclination */ true,
|
||||
new AsyncCallback<String>() {
|
||||
@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<WindSource> 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<WindSource> windSources, final Date from, final Date to,
|
||||
final int selectionVersion, final Runnable onComplete) {
|
||||
final Set<WindSource> 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<WindInfoForRaceDTO>() {
|
||||
@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<WindSource> 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<Void>() {
|
||||
@Override
|
||||
public void onSuccess(final Void result) {
|
||||
}
|
||||
sailingServiceWrite.stopWindLiveSubscription(subscriptionId, new AsyncCallback<Void>() {
|
||||
@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
|
||||
|
||||
+7
@@ -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();
|
||||
|
||||
+7
@@ -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
|
||||
|
||||
+7
@@ -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
|
||||
|
||||
+15
-1
@@ -43,7 +43,7 @@ public class WindChartDataRenderer {
|
||||
private final Map<WindSource, Series> windSourceSpeedSeries = new HashMap<WindSource, Series>();
|
||||
private final Map<WindSource, Point[]> windSourceDirectionPoints = new HashMap<WindSource, Point[]>();
|
||||
private final Map<WindSource, Point[]> windSourceSpeedPoints = new HashMap<WindSource, Point[]>();
|
||||
private final ColorMapImpl<WindSource> colorMap = new ColorMapImpl<WindSource>();
|
||||
private final ColorMapImpl<WindSource> 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<WindSource> 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<WindSource> colorMap) {
|
||||
this(chart, stringMessages, null, addSeriesToChartOnCreation, seriesPointsUpdater, null, colorMap);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user