mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-16 10:48:47 +00:00
bug6150: stats fixes
This commit is contained in:
+116
-26
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -83,3 +83,9 @@
|
||||
border-left: 1px solid #cccccc;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.sourceFillerLabel {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
+29
-60
@@ -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>();
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ public interface WindChartSettingsDialogCssResources extends ClientBundle {
|
||||
String statButtonSectionDisabled();
|
||||
String toAllSelectedLabel();
|
||||
String sourceCheckboxIndent();
|
||||
String sourceFillerLabel();
|
||||
String accentCheckbox();
|
||||
String sectionDivider();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user