bug6150: stats fixes

This commit is contained in:
Masha Kashirina
2026-07-27 01:48:39 +02:00
parent f631962d77
commit 060c256f9f
4 changed files with 152 additions and 86 deletions
@@ -2,6 +2,7 @@ package com.sap.sailing.gwt.ui.client.shared.charts;
import java.util.Arrays;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -87,11 +88,11 @@ public class WindChart extends AbstractRaceChart<WindChartSettings> implements R
private final Map<WindSource, PlotLine> speedAvgPlotLines = new HashMap<WindSource, PlotLine>();
private final Map<WindSource, PlotLine> speedMinPlotLines = new HashMap<WindSource, PlotLine>();
private final Map<WindSource, PlotLine> speedMaxPlotLines = new HashMap<WindSource, PlotLine>();
private final Map<WindSource, StatAccumulator> directionAccumulators = new HashMap<WindSource, StatAccumulator>();
private final Map<WindSource, StatAccumulator> speedAccumulators = new HashMap<WindSource, StatAccumulator>();
private Long timeOfEarliestRequestInMillis;
private Long timeOfLatestRequestInMillis;
private Long zoomFromMillis;
private Long zoomToMillis;
private final ColorMapImpl<WindSource> colorMap;
@@ -382,6 +383,15 @@ public class WindChart extends AbstractRaceChart<WindChartSettings> implements R
final Point[] directionPoints = new Point[windTrackInfo.windFixes.size()];
final Point[] speedPoints = new Point[windTrackInfo.windFixes.size()];
int currentPointIndex = 0;
if (!append) {
directionAccumulators.put(windSource, new StatAccumulator(true));
if (windSource.getType().useSpeed()) {
speedAccumulators.put(windSource, new StatAccumulator(false));
}
}
final StatAccumulator dirAccumulator = directionAccumulators.computeIfAbsent(windSource, s -> new StatAccumulator(true));
final StatAccumulator spdAccumulator = windSource.getType().useSpeed()
? speedAccumulators.computeIfAbsent(windSource, s -> new StatAccumulator(false)) : null;
for (final WindDTO wind : windTrackInfo.windFixes) {
if (newMinTimepoint == null || wind.requestTimepoint < newMinTimepoint) {
newMinTimepoint = wind.requestTimepoint;
@@ -407,8 +417,14 @@ public class WindChart extends AbstractRaceChart<WindChartSettings> implements R
}
directionPoints[currentPointIndex] = newDirectionPoint;
previousDirectionPoint = newDirectionPoint;
if (wind.dampenedTrueWindFromDeg != null) {
dirAccumulator.add(wind.dampenedTrueWindFromDeg);
}
final Point newSpeedPoint = new Point(wind.requestTimepoint, wind.dampenedTrueWindSpeedInKnots);
speedPoints[currentPointIndex++] = newSpeedPoint;
if (spdAccumulator != null && wind.dampenedTrueWindSpeedInKnots != null) {
spdAccumulator.add(wind.dampenedTrueWindSpeedInKnots);
}
}
}
Point[] newDirectionPoints;
@@ -499,27 +515,27 @@ public class WindChart extends AbstractRaceChart<WindChartSettings> implements R
if (!clearCacheAndReload) {
if (dirAvgChanged) {
updateStatPlotLinesForStat(windSourceDirectionPoints, windSourceDirectionSeries, 0,
settings.getDirectionAvgSources(), directionAvgPlotLines, StatKind.AVG, true);
settings.getDirectionAvgSources(), directionAvgPlotLines, directionAccumulators, StatKind.AVG, true);
}
if (dirMinChanged) {
updateStatPlotLinesForStat(windSourceDirectionPoints, windSourceDirectionSeries, 0,
settings.getDirectionMinSources(), directionMinPlotLines, StatKind.MIN, true);
settings.getDirectionMinSources(), directionMinPlotLines, directionAccumulators, StatKind.MIN, true);
}
if (dirMaxChanged) {
updateStatPlotLinesForStat(windSourceDirectionPoints, windSourceDirectionSeries, 0,
settings.getDirectionMaxSources(), directionMaxPlotLines, StatKind.MAX, true);
settings.getDirectionMaxSources(), directionMaxPlotLines, directionAccumulators, StatKind.MAX, true);
}
if (spdAvgChanged) {
updateStatPlotLinesForStat(windSourceSpeedPoints, windSourceSpeedSeries, 1,
settings.getSpeedAvgSources(), speedAvgPlotLines, StatKind.AVG, false);
settings.getSpeedAvgSources(), speedAvgPlotLines, speedAccumulators, StatKind.AVG, false);
}
if (spdMinChanged) {
updateStatPlotLinesForStat(windSourceSpeedPoints, windSourceSpeedSeries, 1,
settings.getSpeedMinSources(), speedMinPlotLines, StatKind.MIN, false);
settings.getSpeedMinSources(), speedMinPlotLines, speedAccumulators, StatKind.MIN, false);
}
if (spdMaxChanged) {
updateStatPlotLinesForStat(windSourceSpeedPoints, windSourceSpeedSeries, 1,
settings.getSpeedMaxSources(), speedMaxPlotLines, StatKind.MAX, false);
settings.getSpeedMaxSources(), speedMaxPlotLines, speedAccumulators, StatKind.MAX, false);
}
}
}
@@ -529,6 +545,8 @@ public class WindChart extends AbstractRaceChart<WindChartSettings> implements R
timeOfLatestRequestInMillis = null;
windSourceDirectionPoints.clear();
windSourceSpeedPoints.clear();
directionAccumulators.clear();
speedAccumulators.clear();
firstPointOfFirstSeries = null;
loadData(timeRangeWithZoomProvider.getFromTime(), timeRangeWithZoomProvider.getToTime(), /* append */false);
}
@@ -593,6 +611,8 @@ public class WindChart extends AbstractRaceChart<WindChartSettings> implements R
private void clearChart() {
chart.removeAllSeries();
removeStatPlotLines();
directionAccumulators.clear();
speedAccumulators.clear();
}
/** Removes all six stat plot lines (avg/min/max for direction and speed) from the chart and clears the maps. */
@@ -613,29 +633,95 @@ public class WindChart extends AbstractRaceChart<WindChartSettings> implements R
/** Which statistic a plot line represents. */
private enum StatKind { AVG, MIN, MAX }
/** Running accumulators for stat computation. Direction uses circular (sin/cos) averaging; speed uses arithmetic. */
private static final class StatAccumulator {
private double sinSum, cosSum, sum, min, max;
private int count;
private final boolean isDirection;
StatAccumulator(final boolean isDirection) {
this.isDirection = isDirection;
reset();
}
void reset() {
sinSum = 0;
cosSum = 0;
sum = 0;
min = Double.MAX_VALUE;
max = -Double.MAX_VALUE;
count = 0;
}
void add(final double v) {
if (isDirection) {
final double radians = Math.toRadians(v);
sinSum += Math.sin(radians);
cosSum += Math.cos(radians);
} else {
sum += v;
}
if (v < min) { min = v; }
if (v > max) { max = v; }
count++;
}
Map<StatKind, Double> toStatMap() {
if (count == 0) {
return null;
}
final Map<StatKind, Double> result = new EnumMap<StatKind, Double>(StatKind.class);
if (isDirection) {
final double avg = Math.toDegrees(Math.atan2(sinSum / count, cosSum / count));
result.put(StatKind.AVG, avg < 0 ? avg + 360 : avg);
result.put(StatKind.MIN, ((min % 360) + 360) % 360);
result.put(StatKind.MAX, ((max % 360) + 360) % 360);
} else {
result.put(StatKind.AVG, sum / count);
result.put(StatKind.MIN, min);
result.put(StatKind.MAX, max);
}
return result;
}
}
/** Computes avg/min/max over a point array. Returns null if there are no valid points.
* If fromMillis/toMillis are non-null, only points within that time window are included. */
private static double[] computeStatValues(final Point[] points, final Long fromMillis, final Long toMillis) {
double sum = 0, min = Double.MAX_VALUE, max = -Double.MAX_VALUE;
* If fromMillis/toMillis are non-null, only points within that time window are included.
* For direction series, the average is computed using circular (vector) averaging so that
* the 0/360 wrap-around is handled correctly; min/max are normalized back to 0-360. */
private static Map<StatKind, Double> computeStatValues(final Point[] points, final Long fromMillis, final Long toMillis,
final boolean isDirection) {
double sinSum = 0, cosSum = 0, sum = 0, min = Double.MAX_VALUE, max = -Double.MAX_VALUE;
int count = 0;
for (final Point p : points) {
if (p != null && p.getY() != null) {
if ((fromMillis == null || p.getX().longValue() >= fromMillis)
&& (toMillis == null || p.getX().longValue() <= toMillis)) {
final double v = p.getY().doubleValue();
sum += v;
if (isDirection) {
final double radians = Math.toRadians(v);
sinSum += Math.sin(radians);
cosSum += Math.cos(radians);
} else {
sum += v;
}
if (v < min) { min = v; }
if (v > max) { max = v; }
count++;
}
}
}
final double[] result;
final Map<StatKind, Double> result;
if (count == 0) {
result = null;
} else {
result = new double[] { sum / count, min, max };
result = new EnumMap<StatKind, Double>(StatKind.class);
if (isDirection) {
final double avg = Math.toDegrees(Math.atan2(sinSum / count, cosSum / count));
result.put(StatKind.AVG, avg < 0 ? avg + 360 : avg);
result.put(StatKind.MIN, ((min % 360) + 360) % 360);
result.put(StatKind.MAX, ((max % 360) + 360) % 360);
} else {
result.put(StatKind.AVG, sum / count);
result.put(StatKind.MIN, min);
result.put(StatKind.MAX, max);
}
}
return result;
}
@@ -694,6 +780,7 @@ public class WindChart extends AbstractRaceChart<WindChartSettings> implements R
final int yAxisIndex,
final Set<WindSourceType> enabledTypes,
final Map<WindSource, PlotLine> plotLineMap,
final Map<WindSource, StatAccumulator> accumulators,
final StatKind kind,
final boolean isDirection) {
for (final PlotLine pl : plotLineMap.values()) {
@@ -701,16 +788,23 @@ public class WindChart extends AbstractRaceChart<WindChartSettings> implements R
}
plotLineMap.clear();
final NumberFormat fmt = NumberFormat.getFormat("0.#");
final Util.Pair<Date, Date> zoom = timeRangeWithZoomProvider.isZoomed() ? timeRangeWithZoomProvider.getTimeZoom() : null;
for (final Map.Entry<WindSource, Point[]> entry : pointsMap.entrySet()) {
final WindSource source = entry.getKey();
final Series series = seriesMap.get(source);
if (series != null && series.isVisible() && entry.getValue() != null
&& enabledTypes.contains(source.getType())) {
final double[] stats = computeStatValues(entry.getValue(), zoomFromMillis, zoomToMillis);
final Map<StatKind, Double> stats;
if (zoom == null) {
final StatAccumulator acc = accumulators.get(source);
stats = acc == null ? null : acc.toStatMap();
} else {
stats = computeStatValues(entry.getValue(), zoom.getA().getTime(), zoom.getB().getTime(), isDirection);
}
if (stats != null) {
final String color = colorMap.getColorByID(source).getAsHtml();
final String sourceName = WindSourceTypeFormatter.format(source, stringMessages);
final double statValue = kind == StatKind.AVG ? stats[0] : (kind == StatKind.MIN ? stats[1] : stats[2]);
final double statValue = stats.get(kind);
final String kindLabel = kind == StatKind.AVG ? stringMessages.windStatAvg()
: (kind == StatKind.MIN ? stringMessages.windStatMin() : stringMessages.windStatMax());
final String unit = isDirection ? stringMessages.degreesShort() : stringMessages.knotsUnit();
@@ -728,17 +822,17 @@ public class WindChart extends AbstractRaceChart<WindChartSettings> implements R
* in the dialog, updateStatPlotLinesForStat is called directly for just that one. */
private void updateStatPlotLines() {
updateStatPlotLinesForStat(windSourceDirectionPoints, windSourceDirectionSeries, 0,
settings.getDirectionAvgSources(), directionAvgPlotLines, StatKind.AVG, true);
settings.getDirectionAvgSources(), directionAvgPlotLines, directionAccumulators, StatKind.AVG, true);
updateStatPlotLinesForStat(windSourceDirectionPoints, windSourceDirectionSeries, 0,
settings.getDirectionMinSources(), directionMinPlotLines, StatKind.MIN, true);
settings.getDirectionMinSources(), directionMinPlotLines, directionAccumulators, StatKind.MIN, true);
updateStatPlotLinesForStat(windSourceDirectionPoints, windSourceDirectionSeries, 0,
settings.getDirectionMaxSources(), directionMaxPlotLines, StatKind.MAX, true);
settings.getDirectionMaxSources(), directionMaxPlotLines, directionAccumulators, StatKind.MAX, true);
updateStatPlotLinesForStat(windSourceSpeedPoints, windSourceSpeedSeries, 1,
settings.getSpeedAvgSources(), speedAvgPlotLines, StatKind.AVG, false);
settings.getSpeedAvgSources(), speedAvgPlotLines, speedAccumulators, StatKind.AVG, false);
updateStatPlotLinesForStat(windSourceSpeedPoints, windSourceSpeedSeries, 1,
settings.getSpeedMinSources(), speedMinPlotLines, StatKind.MIN, false);
settings.getSpeedMinSources(), speedMinPlotLines, speedAccumulators, StatKind.MIN, false);
updateStatPlotLinesForStat(windSourceSpeedPoints, windSourceSpeedSeries, 1,
settings.getSpeedMaxSources(), speedMaxPlotLines, StatKind.MAX, false);
settings.getSpeedMaxSources(), speedMaxPlotLines, speedAccumulators, StatKind.MAX, false);
}
/**
@@ -792,16 +886,12 @@ public class WindChart extends AbstractRaceChart<WindChartSettings> implements R
@Override
public void onTimeZoomChanged(final Date zoomStartTimepoint, final Date zoomEndTimepoint) {
super.onTimeZoomChanged(zoomStartTimepoint, zoomEndTimepoint);
zoomFromMillis = zoomStartTimepoint.getTime();
zoomToMillis = zoomEndTimepoint.getTime();
updateStatPlotLines();
}
@Override
public void onTimeZoomReset() {
super.onTimeZoomReset();
zoomFromMillis = null;
zoomToMillis = null;
updateStatPlotLines();
}
@@ -83,3 +83,9 @@
border-left: 1px solid #cccccc;
margin: 0 8px;
}
.sourceFillerLabel {
color: #aaa;
font-size: 12px;
padding: 2px 0;
}
@@ -261,6 +261,11 @@ public class WindChartSettingsDialogComponent implements SettingsDialogComponent
int row = startRow + 1;
for (final WindSourceType type : ALL_SOURCE_TYPES) {
if (!directionSection && !type.useSpeed()) {
table.getCellFormatter().addStyleName(row, 0, CSS.sourceCheckboxIndent());
final Label filler = new Label(WindSourceTypeFormatter.format(type, stringMessages));
filler.addStyleName(CSS.sourceFillerLabel());
table.setWidget(row, 0, filler);
row++;
continue;
}
final boolean sourceSelected = selectedSources.contains(type);
@@ -323,69 +328,33 @@ public class WindChartSettingsDialogComponent implements SettingsDialogComponent
table.setWidget(row, 3, maxToggle.asWidget());
row++;
}
bulkAvg.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
bulkAvg.toggle();
if (bulkAvg.isOn()) {
for (final Map.Entry<WindSourceType, CheckBox> e : sourceCheckboxes.entrySet()) {
if (e.getValue().getValue()) {
avgToggles.get(e.getKey()).setState(StatState.ON);
avgToggles.get(e.getKey()).asWidget().setVisible(false);
}
}
} else {
for (final Map.Entry<WindSourceType, CheckBox> e : sourceCheckboxes.entrySet()) {
if (e.getValue().getValue()) {
avgToggles.get(e.getKey()).asWidget().setVisible(true);
}
}
}
}
});
bulkMin.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
bulkMin.toggle();
if (bulkMin.isOn()) {
for (final Map.Entry<WindSourceType, CheckBox> e : sourceCheckboxes.entrySet()) {
if (e.getValue().getValue()) {
minToggles.get(e.getKey()).setState(StatState.ON);
minToggles.get(e.getKey()).asWidget().setVisible(false);
}
}
} else {
for (final Map.Entry<WindSourceType, CheckBox> e : sourceCheckboxes.entrySet()) {
if (e.getValue().getValue()) {
minToggles.get(e.getKey()).asWidget().setVisible(true);
}
}
}
}
});
bulkMax.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
bulkMax.toggle();
if (bulkMax.isOn()) {
for (final Map.Entry<WindSourceType, CheckBox> e : sourceCheckboxes.entrySet()) {
if (e.getValue().getValue()) {
maxToggles.get(e.getKey()).setState(StatState.ON);
maxToggles.get(e.getKey()).asWidget().setVisible(false);
}
}
} else {
for (final Map.Entry<WindSourceType, CheckBox> e : sourceCheckboxes.entrySet()) {
if (e.getValue().getValue()) {
maxToggles.get(e.getKey()).asWidget().setVisible(true);
}
}
}
}
});
addBulkClickHandler(bulkAvg, avgToggles, sourceCheckboxes);
addBulkClickHandler(bulkMin, minToggles, sourceCheckboxes);
addBulkClickHandler(bulkMax, maxToggles, sourceCheckboxes);
return new StatToggle[]{bulkAvg, bulkMin, bulkMax};
}
private void addBulkClickHandler(final StatToggle bulk, final Map<WindSourceType, StatToggle> perSourceToggles,
final Map<WindSourceType, CheckBox> sourceCheckboxes) {
bulk.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
bulk.toggle();
for (final Map.Entry<WindSourceType, CheckBox> e : sourceCheckboxes.entrySet()) {
if (e.getValue().getValue()) {
final StatToggle toggle = perSourceToggles.get(e.getKey());
if (bulk.isOn()) {
toggle.setState(StatState.ON);
toggle.asWidget().setVisible(false);
} else {
toggle.asWidget().setVisible(true);
}
}
}
}
});
}
@Override
public WindChartSettings getResult() {
final Set<WindSourceType> dirSources = new HashSet<WindSourceType>();
@@ -19,6 +19,7 @@ public interface WindChartSettingsDialogCssResources extends ClientBundle {
String statButtonSectionDisabled();
String toAllSelectedLabel();
String sourceCheckboxIndent();
String sourceFillerLabel();
String accentCheckbox();
String sectionDivider();
}