diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PolarSheetGenerationSettings.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PolarSheetGenerationSettings.java index 909f661a131..821ff5cb33d 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PolarSheetGenerationSettings.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PolarSheetGenerationSettings.java @@ -30,4 +30,6 @@ public interface PolarSheetGenerationSettings extends Serializable{ boolean splitByWindgauges(); + boolean areDefault(); + } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PolarSheetsData.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PolarSheetsData.java index 9b6ac7c36b3..26f46b1853e 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PolarSheetsData.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PolarSheetsData.java @@ -8,10 +8,8 @@ public interface PolarSheetsData extends Serializable { Number[][] getAveragedPolarDataByWindSpeed(); int getDataCount(); - - boolean isComplete(); - Integer[] getDataCountPerAngleForWindspeed(int beaufort); + Integer[] getDataCountPerAngleForWindspeed(int windIndex); WindStepping getStepping(); diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PolarSheetsXYDiagramData.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PolarSheetsXYDiagramData.java new file mode 100644 index 00000000000..9311cda48d5 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PolarSheetsXYDiagramData.java @@ -0,0 +1,20 @@ +package com.sap.sailing.domain.common; + +import java.io.Serializable; +import java.util.List; + +import com.sap.sse.common.Util.Pair; + +public interface PolarSheetsXYDiagramData extends Serializable { + + List> getPointsForUpwindStarboardAverageSpeed(); + + List> getPointsForUpwindStarboardAverageAngle(); + + List> getPointsForUpwindStarboardAverageAngleMovingAverage(); + + List> getPointsForUpwindStarboardAverageSpeedMovingAverage(); + + List> getPointsForUpwindStarboardAverageConfidence(); + +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindStepping.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindStepping.java index 4f39dbfdfc9..d2f38855204 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindStepping.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/WindStepping.java @@ -6,8 +6,18 @@ public interface WindStepping extends Serializable{ public abstract int getLevelIndexForValue(double speed); - public abstract int getSteppedValueForValue(double speed); + public abstract Double getSteppedValueForValue(double speed); - Integer[] getRawStepping(); + Double[] getRawStepping(); + + public abstract int getLevelIndexFloorForValue(double speed); + + public abstract int getLevelIndexCeilingForValue(double speed); + + double getDistanceToLevelFloor(double speed); + + int hashCode(); + + boolean equals(Object obj); } \ No newline at end of file diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/PolarSheetGenerationSettingsImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/PolarSheetGenerationSettingsImpl.java index 9d5be9aef52..200d58c7bff 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/PolarSheetGenerationSettingsImpl.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/PolarSheetGenerationSettingsImpl.java @@ -1,13 +1,28 @@ package com.sap.sailing.domain.common.impl; +import java.util.ArrayList; +import java.util.List; + import com.sap.sailing.domain.common.PolarSheetGenerationSettings; public class PolarSheetGenerationSettingsImpl implements PolarSheetGenerationSettings { public static PolarSheetGenerationSettings createStandardPolarSettings() { - Integer[] levels = { 4, 6, 8, 10, 12, 14, 16, 20, 25, 30 }; - WindSteppingWithMaxDistance windStepping = new WindSteppingWithMaxDistance(levels, 2.0); - return new PolarSheetGenerationSettingsImpl(200, 0.1, 10, 20, 0.1, true, true, 2, 0.05, true, windStepping, + Double[] levels = { 4., 6., 8., 10., 12., 14., 16., 20., 25., 30. }; + WindSteppingWithMaxDistance windStepping = new WindSteppingWithMaxDistance(levels, 2.5); + return new PolarSheetGenerationSettingsImpl(50, 0.1, 20, 20, 0.1, true, true, 2, 0.05, true, windStepping, + false); + } + + public static PolarSheetGenerationSettings createBackendPolarSettings() { + List levelList = new ArrayList(); + for (double levelValue = 0.5; levelValue < 35; levelValue = levelValue + 0.5) { + levelList.add(levelValue); + } + Double[] levels = levelList.toArray(new Double[levelList.size()]); + + WindSteppingWithMaxDistance windStepping = new WindSteppingWithMaxDistance(levels, 0.5); + return new PolarSheetGenerationSettingsImpl(50, 0.1, 20, 20, 0.1, true, true, 2, 0.05, true, windStepping, false); } @@ -108,4 +123,85 @@ public class PolarSheetGenerationSettingsImpl implements PolarSheetGenerationSet return splitByWindGauges; } + @Override + public boolean areDefault() { + return createStandardPolarSettings().equals(this); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + long temp; + temp = Double.doubleToLongBits(minimumConfidenceMeasure); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + ((minimumDataCountPerAngle == null) ? 0 : minimumDataCountPerAngle.hashCode()); + result = prime * result + ((minimumDataCountPerGraph == null) ? 0 : minimumDataCountPerGraph.hashCode()); + temp = Double.doubleToLongBits(minimumWindConfidence); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + ((numberOfHistogramColumns == null) ? 0 : numberOfHistogramColumns.hashCode()); + temp = Double.doubleToLongBits(outlierDetectionNeighboorhoodRadius); + result = prime * result + (int) (temp ^ (temp >>> 32)); + temp = Double.doubleToLongBits(outlierMinimumNeighboorhoodPct); + result = prime * result + (int) (temp ^ (temp >>> 32)); + result = prime * result + (shouldRemoveOutliers ? 1231 : 1237); + result = prime * result + (splitByWindGauges ? 1231 : 1237); + result = prime * result + (useOnlyEstimationForWindDirection ? 1231 : 1237); + result = prime * result + (useOnlyWindGaugesForWindSpeed ? 1231 : 1237); + result = prime * result + ((windStepping == null) ? 0 : windStepping.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PolarSheetGenerationSettingsImpl other = (PolarSheetGenerationSettingsImpl) obj; + if (Double.doubleToLongBits(minimumConfidenceMeasure) != Double + .doubleToLongBits(other.minimumConfidenceMeasure)) + return false; + if (minimumDataCountPerAngle == null) { + if (other.minimumDataCountPerAngle != null) + return false; + } else if (!minimumDataCountPerAngle.equals(other.minimumDataCountPerAngle)) + return false; + if (minimumDataCountPerGraph == null) { + if (other.minimumDataCountPerGraph != null) + return false; + } else if (!minimumDataCountPerGraph.equals(other.minimumDataCountPerGraph)) + return false; + if (Double.doubleToLongBits(minimumWindConfidence) != Double.doubleToLongBits(other.minimumWindConfidence)) + return false; + if (numberOfHistogramColumns == null) { + if (other.numberOfHistogramColumns != null) + return false; + } else if (!numberOfHistogramColumns.equals(other.numberOfHistogramColumns)) + return false; + if (Double.doubleToLongBits(outlierDetectionNeighboorhoodRadius) != Double + .doubleToLongBits(other.outlierDetectionNeighboorhoodRadius)) + return false; + if (Double.doubleToLongBits(outlierMinimumNeighboorhoodPct) != Double + .doubleToLongBits(other.outlierMinimumNeighboorhoodPct)) + return false; + if (shouldRemoveOutliers != other.shouldRemoveOutliers) + return false; + if (splitByWindGauges != other.splitByWindGauges) + return false; + if (useOnlyEstimationForWindDirection != other.useOnlyEstimationForWindDirection) + return false; + if (useOnlyWindGaugesForWindSpeed != other.useOnlyWindGaugesForWindSpeed) + return false; + if (windStepping == null) { + if (other.windStepping != null) + return false; + } else if (!windStepping.equals(other.windStepping)) + return false; + return true; + } + + } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/PolarSheetsDataImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/PolarSheetsDataImpl.java index 2a5c10dca86..8a3696069cb 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/PolarSheetsDataImpl.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/PolarSheetsDataImpl.java @@ -12,8 +12,6 @@ public class PolarSheetsDataImpl implements PolarSheetsData { private Number[][] averagedPolarDataByWindSpeed; - private boolean complete; - private int dataCount; private WindStepping stepping; @@ -25,9 +23,10 @@ public class PolarSheetsDataImpl implements PolarSheetsData { //For GWT Serialization PolarSheetsDataImpl() {}; - public PolarSheetsDataImpl(Number[][] averagedPolarDataByWindSpeed, boolean complete, int dataCount, Map dataCountPerAngleForWindspeed, WindStepping stepping, Map> histogramDataMap) { + public PolarSheetsDataImpl(Number[][] averagedPolarDataByWindSpeed, int dataCount, + Map dataCountPerAngleForWindspeed, WindStepping stepping, + Map> histogramDataMap) { this.averagedPolarDataByWindSpeed = averagedPolarDataByWindSpeed; - this.complete = complete; this.dataCount = dataCount; this.dataCountPerAngleForWindspeed = dataCountPerAngleForWindspeed; this.stepping = stepping; @@ -40,11 +39,6 @@ public class PolarSheetsDataImpl implements PolarSheetsData { return averagedPolarDataByWindSpeed; } - @Override - public boolean isComplete() { - return complete; - } - @Override public int getDataCount() { return dataCount; diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/PolarSheetsXYDiagramDataImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/PolarSheetsXYDiagramDataImpl.java new file mode 100644 index 00000000000..9c51a57807b --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/PolarSheetsXYDiagramDataImpl.java @@ -0,0 +1,57 @@ +package com.sap.sailing.domain.common.impl; + +import java.util.List; + +import com.sap.sailing.domain.common.PolarSheetsXYDiagramData; +import com.sap.sse.common.Util.Pair; + +public class PolarSheetsXYDiagramDataImpl implements PolarSheetsXYDiagramData { + + private static final long serialVersionUID = 778667444303004468L; + + PolarSheetsXYDiagramDataImpl() {} + + private List> pointsForUpwindStarboardAverageAngle; + private List> pointsForUpwindStarboardAverageSpeed; + private List> pointsForUpwindStarboardAverageAngleMovingAverage; + private List> pointsForUpwindStarboardAverageSpeedMovingAverage; + private List> pointsForUpwindStarboardAverageConfidence; + + public PolarSheetsXYDiagramDataImpl(List> pointsForUpwindStarboardAverageAngle, + List> pointsForUpwindStarboardAverageSpeed, + List> pointsForUpwindStarboardAverageAngleMovingAverage, + List> pointsForUpwindStarboardAverageSpeedMovingAverage, + List> pointsForUpwindStarboardAverageConfidence) { + this.pointsForUpwindStarboardAverageAngle = pointsForUpwindStarboardAverageAngle; + this.pointsForUpwindStarboardAverageSpeed = pointsForUpwindStarboardAverageSpeed; + this.pointsForUpwindStarboardAverageAngleMovingAverage = pointsForUpwindStarboardAverageAngleMovingAverage; + this.pointsForUpwindStarboardAverageSpeedMovingAverage = pointsForUpwindStarboardAverageSpeedMovingAverage; + this.pointsForUpwindStarboardAverageConfidence = pointsForUpwindStarboardAverageConfidence; + } + + @Override + public List> getPointsForUpwindStarboardAverageAngle() { + return pointsForUpwindStarboardAverageAngle; + } + + @Override + public List> getPointsForUpwindStarboardAverageSpeed() { + return pointsForUpwindStarboardAverageSpeed; + } + + @Override + public List> getPointsForUpwindStarboardAverageAngleMovingAverage() { + return pointsForUpwindStarboardAverageAngleMovingAverage; + } + + @Override + public List> getPointsForUpwindStarboardAverageSpeedMovingAverage() { + return pointsForUpwindStarboardAverageSpeedMovingAverage; + } + + @Override + public List> getPointsForUpwindStarboardAverageConfidence() { + return pointsForUpwindStarboardAverageConfidence; + } + +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/WindSteppingImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/WindSteppingImpl.java index c6b0bb04082..4d289158fae 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/WindSteppingImpl.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/WindSteppingImpl.java @@ -1,5 +1,7 @@ package com.sap.sailing.domain.common.impl; +import java.util.Arrays; + import com.sap.sailing.domain.common.WindStepping; public class WindSteppingImpl implements WindStepping { @@ -9,9 +11,9 @@ public class WindSteppingImpl implements WindStepping { protected WindSteppingImpl() {}; private static final long serialVersionUID = 2215693490331489508L; - protected Integer[] levels; + protected Double[] levels; - public WindSteppingImpl(Integer[] levels) { + public WindSteppingImpl(Double[] levels) { this.levels = levels; } @@ -22,7 +24,7 @@ public class WindSteppingImpl implements WindStepping { @Override public int getLevelIndexForValue(double speed) { for (int i = 0; i < levels.length - 1; i++) { - if (speed < levels[i] + ((levels[i+1] - levels[i]) / 2)) { + if (speed < levels[i] + ((levels[i+1] - levels[i]) / 2.)) { return i; } } @@ -30,18 +32,78 @@ public class WindSteppingImpl implements WindStepping { } @Override - public Integer[] getRawStepping() { + public Double[] getRawStepping() { return levels; } @Override - public int getSteppedValueForValue(double speed) { + public Double getSteppedValueForValue(double speed) { int levelIndex = getLevelIndexForValue(speed); - int result = - 1; + Double result = -1.0; if (levelIndex >= 0) { result = levels[levelIndex]; } return result; } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(levels); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + WindSteppingImpl other = (WindSteppingImpl) obj; + if (!Arrays.equals(levels, other.levels)) + return false; + return true; + } + + @Override + public int getLevelIndexFloorForValue(double speed) { + int result = -1; + for (int i = 0; i < levels.length - 1; i++) { + if (speed >= levels[i]) { + result = i; + } + } + return result; + } + + @Override + public double getDistanceToLevelFloor(double speed) { + double result = 0; + int floor = getLevelIndexFloorForValue(speed); + if (floor == -1) { + result = 0; + } else { + result = speed - levels[floor]; + } + return result; + } + + @Override + public int getLevelIndexCeilingForValue(double speed) { + int floor = getLevelIndexFloorForValue(speed); + int result; + if (floor == -1) { + result = -1; + } + if (levels[floor] == speed) { + result = floor; + } else { + result = floor + 1; + } + return result; + } + } diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/WindSteppingWithMaxDistance.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/WindSteppingWithMaxDistance.java index 68e9353db0e..d22d4b35091 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/WindSteppingWithMaxDistance.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/WindSteppingWithMaxDistance.java @@ -11,7 +11,7 @@ public class WindSteppingWithMaxDistance extends WindSteppingImpl { super(); }; - public WindSteppingWithMaxDistance(Integer[] levels, double maxDistance) { + public WindSteppingWithMaxDistance(Double[] levels, double maxDistance) { super(levels); this.maxDistance = maxDistance; } @@ -20,7 +20,7 @@ public class WindSteppingWithMaxDistance extends WindSteppingImpl { public int getLevelIndexForValue(double speed) { int result = -1; for (int i = 0; i < levels.length - 1; i++) { - double threshold = levels[i] + ((levels[i+1] - levels[i]) / 2); + double threshold = levels[i] + ((levels[i+1] - levels[i]) / 2.); if (speed < threshold) { if (threshold - speed <= maxDistance * 2) { result = i; @@ -39,4 +39,30 @@ public class WindSteppingWithMaxDistance extends WindSteppingImpl { public double getMaxDistance() { return maxDistance; } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + long temp; + temp = Double.doubleToLongBits(maxDistance); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + WindSteppingWithMaxDistance other = (WindSteppingWithMaxDistance) obj; + if (Double.doubleToLongBits(maxDistance) != Double.doubleToLongBits(other.maxDistance)) + return false; + return true; + } + + } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/impl/AbstractLogImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/impl/AbstractLogImpl.java index c4c587c3263..2ef838e60a8 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/impl/AbstractLogImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/impl/AbstractLogImpl.java @@ -100,9 +100,9 @@ extends TrackImpl implements AbstractLog { } if (isAdded) { logger.finer(String.format("%s (%s) was added to log %s.", event, event.getClass().getName(), getId())); - onSuccessfulAdd(event, true); + onSuccessfulAdd(event, notifyListeners); } else { - logger.fine(String.format("%s (%s) was not added to race log %s because it already existed there.", event, event.getClass().getName(), getId())); + logger.finer(String.format("%s (%s) was not added to race log %s because it already existed there.", event, event.getClass().getName(), getId())); } return isAdded; } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindSteppingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindSteppingTest.java index 47dbba9300b..440ab223abd 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindSteppingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/WindSteppingTest.java @@ -12,26 +12,26 @@ public class WindSteppingTest { @Test public void testStepping() { - Integer[] levels = {2,4}; + Double[] levels = {2.,4.}; WindStepping stepping = new WindSteppingImpl(levels); Assert.assertEquals(0, stepping.getLevelIndexForValue(1)); Assert.assertEquals(1, stepping.getLevelIndexForValue(5)); Assert.assertEquals(1, stepping.getLevelIndexForValue(3)); - Assert.assertEquals(2, stepping.getSteppedValueForValue(1.064)); - Assert.assertEquals(4, stepping.getSteppedValueForValue(7.8365)); + Assert.assertEquals(2., stepping.getSteppedValueForValue(1.064), 0.005); + Assert.assertEquals(4., stepping.getSteppedValueForValue(7.8365), 0.005); } @Test public void testSteppingWithMaxDistance() { - Integer[] levels = {2,4}; + Double[] levels = {2.,4.}; WindStepping stepping = new WindSteppingWithMaxDistance(levels, 1.0); Assert.assertEquals(0, stepping.getLevelIndexForValue(1)); Assert.assertEquals(1, stepping.getLevelIndexForValue(5)); Assert.assertEquals(-1, stepping.getLevelIndexForValue(5.01)); Assert.assertEquals(-1, stepping.getLevelIndexForValue(0.5)); Assert.assertEquals(-1, stepping.getLevelIndexForValue(8)); - Assert.assertEquals(2, stepping.getSteppedValueForValue(1.064)); - Assert.assertEquals(-1, stepping.getSteppedValueForValue(7.8365)); + Assert.assertEquals(2.0, stepping.getSteppedValueForValue(1.064), 0.005); + Assert.assertEquals(-1.0, stepping.getSteppedValueForValue(7.8365), 0.005); } } diff --git a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF old mode 100644 new mode 100755 index ca5fb94e6b8..5af738d2631 --- a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF @@ -1,38 +1,37 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Domain -Bundle-SymbolicName: com.sap.sailing.domain -Bundle-Version: 1.0.0.qualifier -Bundle-Vendor: SAP -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Bundle-ClassPath: . -Export-Package: com.sap.sailing.domain.base, - com.sap.sailing.domain.base.impl, - com.sap.sailing.domain.confidence, - com.sap.sailing.domain.leaderboard, - com.sap.sailing.domain.leaderboard.caching, - com.sap.sailing.domain.leaderboard.impl, - com.sap.sailing.domain.leaderboard.meta, - com.sap.sailing.domain.markpassingcalculation, - com.sap.sailing.domain.markpassingcalculation.splining, - com.sap.sailing.domain.masterdataimport, - com.sap.sailing.domain.polarsheets, - com.sap.sailing.domain.racelog, - com.sap.sailing.domain.racelog.impl, - com.sap.sailing.domain.racelog.tracking, - com.sap.sailing.domain.trackfiles, - com.sap.sailing.domain.trackimport, - com.sap.sailing.domain.tracking, - com.sap.sailing.domain.tracking.impl, - com.sap.sailing.util, - com.sap.sailing.util.impl;x-friends:="com.sap.sailing.server.replication,com.sap.sailing.domain.test" -Bundle-ActivationPolicy: lazy -Require-Bundle: com.googlecode.java-diff-utils;bundle-version="1.3.0", - com.sap.sailing.domain.shared.android;bundle-version="1.0.0", - com.sap.sailing.domain.common, - org.json.simple;bundle-version="1.1.0", - com.sap.sailing.geocoding, - com.sap.sse.datamining.shared;bundle-version="1.0.0", - com.sap.sse.common, - com.sap.sse, - com.sap.sse.replication +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Domain +Bundle-SymbolicName: com.sap.sailing.domain +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ClassPath: . +Export-Package: com.sap.sailing.domain.base, + com.sap.sailing.domain.base.impl, + com.sap.sailing.domain.confidence, + com.sap.sailing.domain.leaderboard, + com.sap.sailing.domain.leaderboard.caching, + com.sap.sailing.domain.leaderboard.impl, + com.sap.sailing.domain.leaderboard.meta, + com.sap.sailing.domain.markpassingcalculation, + com.sap.sailing.domain.markpassingcalculation.splining, + com.sap.sailing.domain.masterdataimport, + com.sap.sailing.domain.racelog, + com.sap.sailing.domain.racelog.impl, + com.sap.sailing.domain.racelog.tracking, + com.sap.sailing.domain.trackfiles, + com.sap.sailing.domain.trackimport, + com.sap.sailing.domain.tracking, + com.sap.sailing.domain.tracking.impl, + com.sap.sailing.util, + com.sap.sailing.util.impl;x-friends:="com.sap.sailing.server.replication,com.sap.sailing.domain.test" +Bundle-ActivationPolicy: lazy +Require-Bundle: com.googlecode.java-diff-utils;bundle-version="1.3.0", + com.sap.sailing.domain.shared.android;bundle-version="1.0.0", + com.sap.sailing.domain.common, + org.json.simple;bundle-version="1.1.0", + com.sap.sailing.geocoding, + com.sap.sse.datamining.shared;bundle-version="1.0.0", + com.sap.sse.common, + com.sap.sse, + com.sap.sse.replication diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/AngleSpeedOddClassifier.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/AngleSpeedOddClassifier.java deleted file mode 100644 index b6c83ae8499..00000000000 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/AngleSpeedOddClassifier.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.sap.sailing.domain.polarsheets; - -import java.util.logging.Level; -import java.util.logging.Logger; - -public class AngleSpeedOddClassifier implements OddFixClassifier { - - - private static final Logger logger = Logger.getLogger(AngleSpeedOddClassifier.class.getName()); - - @Override - public boolean classifiesAsOdd(PolarFix fix) { - double angleToWind = fix.getAngleToWind(); - double speedInKnots = fix.getBoatSpeed().getKnots(); - if (angleToWind < 20 && angleToWind > -20) { - if (speedInKnots > 2) { - logger.log(Level.INFO, "Boat goes fast against the wind. Data-Point should have been excluded during maneuver check."); - return true; - } - } - return false; - } - -} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/OddFixClassifier.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/OddFixClassifier.java deleted file mode 100644 index 12874568df3..00000000000 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/OddFixClassifier.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.sap.sailing.domain.polarsheets; - - -public interface OddFixClassifier { - - /** - * Used for classifying fixes from a logical perspective. - * - * Example: An OddFixClassifier could check if a boat has high speed when going against the wind. - * - * @param polarFix to be classified - * @return True if odd from a sailing perspective, false if it makes sense. Classification depends on the type of OddFixClassifier used. - */ - public boolean classifiesAsOdd(PolarFix polarFix); - -} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRegatta.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRegatta.java index 442bd672bfd..8e591df0c87 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRegatta.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/DynamicTrackedRegatta.java @@ -22,8 +22,8 @@ public interface DynamicTrackedRegatta extends TrackedRegatta { * may be null which means that no update will be fired to any * {@link DynamicRaceDefinitionSet}. */ - DynamicTrackedRace createTrackedRace(RaceDefinition raceDefinition, Iterable sidelines, WindStore windStore, - GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed, - DynamicRaceDefinitionSet raceDefinitionSetToUpdate); + DynamicTrackedRace createTrackedRace(RaceDefinition raceDefinition, Iterable sidelines, + WindStore windStore, GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, + long millisecondsOverWhichToAverageSpeed, DynamicRaceDefinitionSet raceDefinitionSetToUpdate); } diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/PolarSheetGenerationServiceTest.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/PolarSheetGenerationServiceTest.java index ed5b0bf0344..2f2fff6f716 100755 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/PolarSheetGenerationServiceTest.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/PolarSheetGenerationServiceTest.java @@ -66,7 +66,7 @@ public class PolarSheetGenerationServiceTest { SailingService service = new MockSailingServiceForPolarSheetGeneration(); - Integer[] levels = { 4, 6, 8, 10, 12, 14, 16, 20, 25, 30 }; + Double[] levels = { 4., 6., 8., 10., 12., 14., 16., 20., 25., 30. }; WindSteppingWithMaxDistance windStepping = new WindSteppingWithMaxDistance(levels, 2.0); PolarSheetGenerationSettings settings = new PolarSheetGenerationSettingsImpl(1, 0, 1, 20, 0, false, true, 5, 0.05, false, windStepping, false); @@ -81,7 +81,6 @@ public class PolarSheetGenerationServiceTest { PolarSheetsData results = triggerData.getData(); - Assert.assertTrue(results.isComplete()); Assert.assertNotNull(results); Assert.assertEquals(4, results.getDataCount()); diff --git a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF index afcd59c7a27..6e7d1c6ba34 100755 --- a/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.gwt.ui/META-INF/MANIFEST.MF @@ -47,7 +47,10 @@ Require-Bundle: com.sap.sailing.domain, com.sap.sse.security.common Bundle-Activator: com.sap.sailing.gwt.ui.server.Activator Bundle-ActivationPolicy: lazy -Import-Package: javax.jms;version="1.1.0", +Import-Package: com.sap.sailing.polars, + com.sap.sailing.polars.factory, + com.sap.sailing.polars.regression, + javax.jms;version="1.1.0", javax.servlet;version="3.1.0", javax.servlet.descriptor;version="3.1.0", javax.servlet.http;version="3.1.0", diff --git a/java/com.sap.sailing.gwt.ui/RaceBoard.css b/java/com.sap.sailing.gwt.ui/RaceBoard.css index 15d8919b897..12df746cc51 100644 --- a/java/com.sap.sailing.gwt.ui/RaceBoard.css +++ b/java/com.sap.sailing.gwt.ui/RaceBoard.css @@ -566,14 +566,7 @@ img.openColumn { background-position: 4% 50%, 96% 50% } -.gwt-SplitLayoutPanel-NorthSouthToggleButton-Closed-media { - background-color: white; - background-image: url(images/viewicons/@1x/img/SAP_RV_AudioVideo_INACTIVE.png); - background-repeat: no-repeat, no-repeat; - background-position: 4% 50%, 96% 50% -} - -.gwt-SplitLayoutPanel-NorthSouthToggleButton-Open-media { +.gwt-SplitLayoutPanel-NorthSouthToggleButton-Closed-media, .gwt-SplitLayoutPanel-NorthSouthToggleButton-Open-media { background-color: white; background-image: url(images/viewicons/@1x/img/SAP_RV_AudioVideo_INACTIVE.png); background-repeat: no-repeat, no-repeat; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/client/shared/mainmedia/MainMedia.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/client/shared/mainmedia/MainMedia.java index f281c948d1a..7a344cd5d56 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/client/shared/mainmedia/MainMedia.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/client/shared/mainmedia/MainMedia.java @@ -1,6 +1,7 @@ package com.sap.sailing.gwt.home.client.shared.mainmedia; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Random; @@ -26,13 +27,14 @@ public class MainMedia extends Composite { @UiField HTMLPanel videosPanel; + @UiField DivElement videoLightBoxData; @UiField ImageCarousel imageCarousel; - private int videoCounter; + private final HashSet addedVideoUrls = new HashSet(MAX_VIDEO_COUNT); interface MainMediaUiBinder extends UiBinder { } @@ -40,16 +42,15 @@ public class MainMedia extends Composite { private static MainMediaUiBinder uiBinder = GWT.create(MainMediaUiBinder.class); public MainMedia(PlaceNavigator navigator) { - videoCounter = 0; - MainMediaResources.INSTANCE.css().ensureInjected(); initWidget(uiBinder.createAndBindUi(this)); } public void setFeaturedEvents(List> featuredEvents) { for (Pair featuredEventTypeAndEvent : featuredEvents) { - if (featuredEventTypeAndEvent.getB().getVideoURLs().size() > 0 && videoCounter < MAX_VIDEO_COUNT) { - addVideoToVideoPanel(featuredEventTypeAndEvent.getB()); + if (featuredEventTypeAndEvent.getB().getVideoURLs().size() > 0 && addedVideoUrls.size() < MAX_VIDEO_COUNT) { + String youTubeRandomUrl = getRandomVideoURL(featuredEventTypeAndEvent.getB()); + addVideoToVideoPanel(youTubeRandomUrl, featuredEventTypeAndEvent.getB()); } } } @@ -61,19 +62,41 @@ public class MainMedia extends Composite { this.height = height; this.width = width; } - String url; int height; int width; } - List photoGalleryUrls = new LinkedList<>(); + + final List photoGalleryUrls = new LinkedList<>(); + final List> videoCandidates = new LinkedList<>(); + for (EventBaseDTO event : recentEvents) { for (String url : event.getSailingLovesPhotographyImages()) { ImageSize size = event.getImageSize(url); photoGalleryUrls.add(new Holder(url, size.getHeight(), size.getWidth())); } - if (!event.getVideoURLs().isEmpty() && videoCounter < MAX_VIDEO_COUNT) { - addVideoToVideoPanel(event); + for (String videoUrl : event.getVideoURLs()) { + videoCandidates.add(new Pair(videoUrl, event)); + } + } + + final int numberOfCandidatesAvailable = videoCandidates.size(); + if (numberOfCandidatesAvailable <= (MAX_VIDEO_COUNT - addedVideoUrls.size())) { + // add all we have, no randomize + for (Pair videoCandidateInfo : videoCandidates) { + addVideoToVideoPanel(videoCandidateInfo.getA(), videoCandidateInfo.getB()); + } + } else { + // fill up the list randomly from videoCandidates + final Random videosRandomizer = new Random(numberOfCandidatesAvailable); + randomlyPick: for (int i = 0; i < numberOfCandidatesAvailable; i++) { + int nextVideoindex = videosRandomizer.nextInt(numberOfCandidatesAvailable); + final Pair videoCandidateInfo = videoCandidates.get(nextVideoindex); + final String youtubeUrl = videoCandidateInfo.getA(); + addVideoToVideoPanel(youtubeUrl, videoCandidateInfo.getB()); + if (addedVideoUrls.size() == MAX_VIDEO_COUNT) { + break randomlyPick; + } } } @@ -83,22 +106,21 @@ public class MainMedia extends Composite { for (int i = 0; i < gallerySize; i++) { Collections.swap(photoGalleryUrls, i, random.nextInt(gallerySize)); } - for (Holder holder : photoGalleryUrls) { - imageCarousel.addImage(holder.url, holder.height, holder.width); } - } - private void addVideoToVideoPanel(EventBaseDTO event) { - String youtubeUrl = getRandomVideoURL(event); + private void addVideoToVideoPanel(String youtubeUrl, EventBaseDTO event) { + if (addedVideoUrls.contains(youtubeUrl)) { + return; + } String eventName = event.getName(); String youtubeId = YoutubeApi.getIdByUrl(youtubeUrl); if (youtubeId != null && !youtubeId.trim().isEmpty()) { MainMediaVideo video = new MainMediaVideo(eventName, youtubeId); videosPanel.add(video); - videoCounter++; + addedVideoUrls.add(youtubeUrl); } } @@ -113,4 +135,6 @@ public class MainMedia extends Composite { return result; } + + } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java index a592150db6a..8d1ec7367b6 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingService.java @@ -21,6 +21,7 @@ import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.PassingInstruction; import com.sap.sailing.domain.common.PolarSheetGenerationResponse; import com.sap.sailing.domain.common.PolarSheetGenerationSettings; +import com.sap.sailing.domain.common.PolarSheetsXYDiagramData; import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaIdentifier; @@ -355,10 +356,16 @@ public interface SailingService extends RemoteService { void createCourseArea(UUID eventId, String courseAreaName); + List getBoatClassNamesWithPolarSheetsAvailable(); + void removeCourseArea(UUID eventId, UUID courseAreaId); List> getLeaderboardsNamesOfMetaLeaderboard(String metaLeaderboardName); + PolarSheetGenerationResponse showCachedPolarSheetForBoatClass(String boatClassName); + + + Util.Pair checkLeaderboardName(String leaderboardName); /** for backward compatibility with the regatta overview */ @@ -395,16 +402,16 @@ public interface SailingService extends RemoteService { Iterable getCompetitors(); - /** - * - * @param leaderboardName - * @param lookInRaceLogs If set to {@code true}, the {@link RaceLog}s are checked for the competitor registrations. - * If set to {@code false}, the {@link RaceDefinition}s are checked instead. - * @return - */ - Iterable getCompetitorsOfLeaderboard(String leaderboardName, boolean lookInRaceLogs); + /** + * + * @param leaderboardName + * @param lookInRaceLogs If set to {@code true}, the {@link RaceLog}s are checked for the competitor registrations. + * If set to {@code false}, the {@link RaceDefinition}s are checked instead. + * @return + */ + Iterable getCompetitorsOfLeaderboard(String leaderboardName, boolean lookInRaceLogs); - CompetitorDTO addOrUpdateCompetitor(CompetitorDTO competitor); + CompetitorDTO addOrUpdateCompetitor(CompetitorDTO competitor); void allowCompetitorResetToDefaults(Iterable competitors); @@ -429,68 +436,68 @@ public interface SailingService extends RemoteService { void removeIgtimiAccount(String eMailOfAccountToRemove); Map importWindFromIgtimi(List selectedRaces, boolean correctByDeclination) throws Exception; - - void denoteForRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName) throws Exception; - - /** - * Revoke the {@link DenoteForTrackingEvent}. This does not affect an existing {@code RaceLogRaceTracker} - * or {@link TrackedRace} for this {@code RaceLog}. - * - * @see RaceLogTrackingAdapter#removeDenotationForRaceLogTracking - */ - void removeDenotationForRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName); - - void denoteForRaceLogTracking(String leaderboardName) throws Exception; - - /** - * Performs all the necessary steps to start tracking the race. - * The {@code RaceLog} needs to be denoted for racelog-tracking beforehand. - * - * @see RaceLogTrackingAdapter#startTracking - */ - void startRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName) - throws NotDenotedForRaceLogTrackingException, Exception; - - void setCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName, Set competitors); - - Collection getCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName); - - void addMarkToRaceLog(String leaderboardName, String raceColumnName, String fleetName, MarkDTO markDTO); - - Collection getMarksInRaceLog(String leaderboardName, String raceColumnName, String fleetName); - - /** - * Adds the course definition to the racelog, while trying to reuse existing marks, controlpoints and waypoints - * from the previous course definition in the racelog. - */ - void addCourseDefinitionToRaceLog(String leaderboardName, String raceColumnName, String fleetName, List> course); - - RaceCourseDTO getLastCourseDefinitionInRaceLog(String leaderboardName, String raceColumnName, String fleetName); - - /** - * Adds a fix to the {@link GPSFixStore}, and creates a mapping with a virtual device for exactly the current timepoint. - */ - void pingMarkViaRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, MarkDTO mark, PositionDTO position); - - void copyCourseAndCompetitorsToOtherRaceLogs(Util.Triple raceLogFrom, - Set> raceLogsTo); - - void addDeviceMappingToRaceLog(String leaderboardName, String raceColumnName, String fleetName, DeviceMappingDTO mapping) - throws TransformationException; - - List getDeviceMappingsFromRaceLog(String leaderboardName, String raceColumnName, String fleetName) - throws TransformationException; - - List getDeserializableDeviceIdentifierTypes(); - - void closeOpenEndedDeviceMapping(String leaderboardName, String raceColumnName, String fleetName, DeviceMappingDTO mapping, - Date closingTimePoint) throws NoCorrespondingServiceRegisteredException, TransformationException; - - /** - * Revoke the events in the {@code RaceLog} that are identified by the {@code eventIds}. - * This only affects such events that implement {@link Revokable}. - */ - void revokeRaceLogEvents(String leaderboardName, String raceColumnName, String fleetName, List eventIds) + + void denoteForRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName) throws Exception; + + /** + * Revoke the {@link DenoteForTrackingEvent}. This does not affect an existing {@code RaceLogRaceTracker} + * or {@link TrackedRace} for this {@code RaceLog}. + * + * @see RaceLogTrackingAdapter#removeDenotationForRaceLogTracking + */ + void removeDenotationForRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName); + + void denoteForRaceLogTracking(String leaderboardName) throws Exception; + + /** + * Performs all the necessary steps to start tracking the race. + * The {@code RaceLog} needs to be denoted for racelog-tracking beforehand. + * + * @see RaceLogTrackingAdapter#startTracking + */ + void startRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName) + throws NotDenotedForRaceLogTrackingException, Exception; + + void setCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName, Set competitors); + + Collection getCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName); + + void addMarkToRaceLog(String leaderboardName, String raceColumnName, String fleetName, MarkDTO markDTO); + + Collection getMarksInRaceLog(String leaderboardName, String raceColumnName, String fleetName); + + /** + * Adds the course definition to the racelog, while trying to reuse existing marks, controlpoints and waypoints + * from the previous course definition in the racelog. + */ + void addCourseDefinitionToRaceLog(String leaderboardName, String raceColumnName, String fleetName, List> course); + + RaceCourseDTO getLastCourseDefinitionInRaceLog(String leaderboardName, String raceColumnName, String fleetName); + + /** + * Adds a fix to the {@link GPSFixStore}, and creates a mapping with a virtual device for exactly the current timepoint. + */ + void pingMarkViaRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, MarkDTO mark, PositionDTO position); + + void copyCourseAndCompetitorsToOtherRaceLogs(Util.Triple raceLogFrom, + Set> raceLogsTo); + + void addDeviceMappingToRaceLog(String leaderboardName, String raceColumnName, String fleetName, DeviceMappingDTO mapping) + throws TransformationException; + + List getDeviceMappingsFromRaceLog(String leaderboardName, String raceColumnName, String fleetName) + throws TransformationException; + + List getDeserializableDeviceIdentifierTypes(); + + void closeOpenEndedDeviceMapping(String leaderboardName, String raceColumnName, String fleetName, DeviceMappingDTO mapping, + Date closingTimePoint) throws NoCorrespondingServiceRegisteredException, TransformationException; + + /** + * Revoke the events in the {@code RaceLog} that are identified by the {@code eventIds}. + * This only affects such events that implement {@link Revokable}. + */ + void revokeRaceLogEvents(String leaderboardName, String raceColumnName, String fleetName, List eventIds) throws NotRevokableException; Collection getGPSFixImporterTypes(); @@ -529,6 +536,8 @@ public interface SailingService extends RemoteService { */ RaceDTO setStartTimeReceivedForRace(RaceIdentifier raceIdentifier, Date newStartTimeReceived); + PolarSheetsXYDiagramData createXYDiagramForBoatClass(String itemText); + /** * Imports regatta structure definitions from an ISAF XRR document * diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java index cd773b9f078..69c619f7651 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/SailingServiceAsync.java @@ -16,6 +16,7 @@ import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sailing.domain.common.PassingInstruction; import com.sap.sailing.domain.common.PolarSheetGenerationResponse; import com.sap.sailing.domain.common.PolarSheetGenerationSettings; +import com.sap.sailing.domain.common.PolarSheetsXYDiagramData; import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaIdentifier; @@ -516,10 +517,10 @@ public interface SailingServiceAsync extends BuildVersionRetriever { void getCompetitors(AsyncCallback> asyncCallback); - void getCompetitorsOfLeaderboard(String leaderboardName, boolean lookInRaceLogs, - AsyncCallback> asyncCallback); + void getCompetitorsOfLeaderboard(String leaderboardName, boolean lookInRaceLogs, + AsyncCallback> asyncCallback); - void addOrUpdateCompetitor(CompetitorDTO competitor, AsyncCallback asyncCallback); + void addOrUpdateCompetitor(CompetitorDTO competitor, AsyncCallback asyncCallback); void allowCompetitorResetToDefaults(Iterable competitors, AsyncCallback asyncCallback); @@ -554,60 +555,65 @@ public interface SailingServiceAsync extends BuildVersionRetriever { void importWindFromIgtimi(List selectedRaces, boolean correctByDeclination, AsyncCallback> asyncCallback); + void getBoatClassNamesWithPolarSheetsAvailable(AsyncCallback> asyncCallback); + void getEventById(UUID id, boolean withStatisticalData, AsyncCallback callback); + + void showCachedPolarSheetForBoatClass(String boatClassName, + AsyncCallback asyncCallback); void getLeaderboardsByEvent(EventDTO event, AsyncCallback> callback); - void denoteForRaceLogTracking(String leaderboardName, - String raceColumnName, String fleetName, - AsyncCallback callback); - - void denoteForRaceLogTracking(String leaderboardName, AsyncCallback callback); - - void startRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, - AsyncCallback callback); - - void setCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName, - Set competitors, AsyncCallback callback); - - void getCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName, - AsyncCallback> callback); - - void addMarkToRaceLog(String leaderboardName, String raceColumnName, String fleetName, MarkDTO markDTO, - AsyncCallback callback); - - void getMarksInRaceLog(String leaderboardName, String raceColumnName, String fleetName, - AsyncCallback> callback); - - void addCourseDefinitionToRaceLog(String leaderboardName, String raceColumnName, String fleetName, - List> course, AsyncCallback callback); - - void getLastCourseDefinitionInRaceLog(String leaderboardName, String raceColumnName, String fleetName, - AsyncCallback callback); - - void pingMarkViaRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, MarkDTO mark, - PositionDTO position, AsyncCallback callback); - - void getDeserializableDeviceIdentifierTypes(AsyncCallback> callback); - - void getDeviceMappingsFromRaceLog(String leaderboardName, String raceColumnName, String fleetName, - AsyncCallback> callback); - - void addDeviceMappingToRaceLog(String leaderboardName, String raceColumnName, String fleetName, - DeviceMappingDTO mapping, AsyncCallback callback); - - void closeOpenEndedDeviceMapping(String leaderboardName, String raceColumnName, String fleetName, - DeviceMappingDTO mapping, Date closingTimePoint, AsyncCallback callback); - - void revokeRaceLogEvents(String leaderboardName, String raceColumnName, String fleetName, List eventIds, - AsyncCallback callback); - + void denoteForRaceLogTracking(String leaderboardName, + String raceColumnName, String fleetName, + AsyncCallback callback); + + void denoteForRaceLogTracking(String leaderboardName, AsyncCallback callback); + + void startRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback callback); + + void setCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName, + Set competitors, AsyncCallback callback); + + void getCompetitorRegistrations(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback> callback); + + void addMarkToRaceLog(String leaderboardName, String raceColumnName, String fleetName, MarkDTO markDTO, + AsyncCallback callback); + + void getMarksInRaceLog(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback> callback); + + void addCourseDefinitionToRaceLog(String leaderboardName, String raceColumnName, String fleetName, + List> course, AsyncCallback callback); + + void getLastCourseDefinitionInRaceLog(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback callback); + + void pingMarkViaRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, MarkDTO mark, + PositionDTO position, AsyncCallback callback); + + void getDeserializableDeviceIdentifierTypes(AsyncCallback> callback); + + void getDeviceMappingsFromRaceLog(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback> callback); + + void addDeviceMappingToRaceLog(String leaderboardName, String raceColumnName, String fleetName, + DeviceMappingDTO mapping, AsyncCallback callback); + + void closeOpenEndedDeviceMapping(String leaderboardName, String raceColumnName, String fleetName, + DeviceMappingDTO mapping, Date closingTimePoint, AsyncCallback callback); + + void revokeRaceLogEvents(String leaderboardName, String raceColumnName, String fleetName, List eventIds, + AsyncCallback callback); + void removeSeries(RegattaIdentifier regattaIdentifier, String seriesName, AsyncCallback callback); - void removeDenotationForRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, - AsyncCallback callback); - - void copyCourseAndCompetitorsToOtherRaceLogs(Util.Triple raceLogFrom, + void removeDenotationForRaceLogTracking(String leaderboardName, String raceColumnName, String fleetName, + AsyncCallback callback); + + void copyCourseAndCompetitorsToOtherRaceLogs(Util.Triple raceLogFrom, Set> raceLogsTo, AsyncCallback callback); void getGPSFixImporterTypes(AsyncCallback> callback); @@ -643,15 +649,17 @@ public interface SailingServiceAsync extends BuildVersionRetriever { AsyncCallback> callback); void setStartTimeReceivedForRace(RaceIdentifier raceIdentifier, Date newStartTimeReceived, AsyncCallback callback); - + + void createXYDiagramForBoatClass(String itemText, AsyncCallback asyncCallback); + /** * Imports regatta structure definitions from an ISAF XRR document * * @param manage2SailJsonUrl the URL pointing to a Manage2Sail JSON document that contains the link to the XRR document */ void getRegattas(String manage2SailJsonUrl, AsyncCallback> asyncCallback); - + void createRegattaStructure(Iterable regattaNames, - EventDTO newEvent, AsyncCallback asyncCallback); - -} + EventDTO newEvent, AsyncCallback asyncCallback); + +} 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 28a0cb03637..8e5ad4a2f9e 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 @@ -969,6 +969,8 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages { String distanceToLine(); String beatAngleTooltip(); String beatAngle(); + String showBoatClassChartsLabel(); + String showDiagram(); String runAutomaticallyTooltip(); String rerunQueryAfterRefreshTooltip(); String queryDefinitionProvider(); @@ -1117,6 +1119,10 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages { String editSeries(); String pleaseSelectAtLeastOneRegatta(); String pleaseSelectAnEvent(); + String showXYDiagram(); + String xyDiagram(); + String boatSpeed(); + String confidence(); String errorTryingToGetEvents(String message); String createDefaultSettingsForAllRegattas(); String noOverallEventSelected(); 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 c50390bac8d..5a9a1cbd067 100755 --- 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 @@ -953,6 +953,8 @@ timeSinceStart={0} since start distanceToLine=Distance to start line beatAngleTooltip=The angle between the competitors direction and the wind beatAngle=Beat Angle +showBoatClassChartsLabel=You can also view the overall diagram for the available boat classes. +showDiagram=Show Diagram runAutomaticallyTooltip=Run the query automatically, after you changed e.g. the statistic or the grouping. rerunQueryAfterRefreshTooltip=Reruns the query after the tables have been refreshed. queryDefinitionProvider=Query Definition Provider @@ -1100,6 +1102,11 @@ editSeries=Edit Series pleaseSelectAtLeastOneRegatta=Please select at least one regatta pleaseSelectAnEvent=Please select an event errorTryingToGetEvents=Error trying to get events: {0} +showXYDiagram=Show x-y-diagram +xyDiagram=x-y-Diagram +boatSpeed=Boat Speed +confidence=Confidence + createDefaultSettingsForAllRegattas=Create default settings for all regattas... noOverallEventSelected=No overall event selected... errorTryingToCreateNewEvent=Error trying to create new event {0}: {1} 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 4bd5f01f116..4c57cf28f0c 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 @@ -949,6 +949,8 @@ timeSinceStart={0} seit dem Start distanceToLine=Abstand zur Startlinie beatAngleTooltip=Der Winkel zwischen Windrichtung und Fahrtrichtung des Teilnehmers beatAngle=Winkel zum Wind +showBoatClassChartsLabel=Im folgenden können außerdem auch die existierenden Diagramme für die vorhandenen Bootsklassen angezeigt werden. +showDiagram=Diagramm anzeigen runAutomaticallyTooltip=Führe die Anfrage automatisch durch, wenn zum Beispiel die Statistik oder die Gruppierung geändert wurde. rerunQueryAfterRefreshTooltip=Wiederholt die Anfrage nachdem die Tabellen aktualisiert wurden. queryDefinitionProvider=Anfragen-Definitions-Versorger @@ -1098,4 +1100,4 @@ createDefaultSettingsForAllRegattas=Standardeinstellungen für alle Regatten anl noOverallEventSelected=Keine übergeordnete Veranstaltung ausgewählt... errorTryingToCreateNewEvent=Fehler beim Anlegen einer neuen Veranstaltung {0}: {1} createNewEvent=Neue Veranstaltung anlegen -noFleetsDefined=Keine Gruppen definiert. \ No newline at end of file +noFleetsDefined=Keine Gruppen definiert. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index d4c457f068a..4d5f3fc9a8b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -944,6 +944,8 @@ distanceToLine=Растояние до линии старта beatAngleTooltip=Угол между направлением движения участника и ветром beatAngle=Угол лавирования runAutomaticallyTooltip=Запустить запрос автоматически, например, после изменения статистических данных или группировки. +showBoatClassChartsLabel=You can also view the overall diagram for the available boat classes. +showDiagram=Show Diagram rerunQueryAfterRefreshTooltip=Повторно запускает запрос после обновления таблиц. queryDefinitionProvider=Поставщик определения запроса statisticProvider=Поставщик статистических данных @@ -1046,4 +1048,4 @@ createDefaultSettingsForAllRegattas=Create default settings for all regattas... noOverallEventSelected=No overall event selected... errorTryingToCreateNewEvent=Error trying to create new event {0}: {1} createNewEvent=Create New Event -noFleetsDefined=No fleets defined. \ No newline at end of file +noFleetsDefined=No fleets defined. diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/presentation/PolarChart.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/presentation/PolarChart.java new file mode 100644 index 00000000000..33a19d28831 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/datamining/client/presentation/PolarChart.java @@ -0,0 +1,89 @@ +package com.sap.sailing.gwt.ui.datamining.client.presentation; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.Series; +import org.moxieapps.gwt.highcharts.client.plotOptions.LinePlotOptions; + +import com.google.gwt.user.client.ui.Widget; +import com.sap.sailing.gwt.ui.datamining.ResultsPresenter; +import com.sap.sse.datamining.shared.GroupKey; +import com.sap.sse.datamining.shared.QueryResult; + +/** + * Expects the inner key in the group key to be the angle for the polar chart. (x-value) Maybe the angle should be + * transfered as part of the result value in the future. But in the backend the angle is treated as a key (for polar + * sheets) for grouping all polar fixes with the same rounded angle + * + * @author Frederik Petersen D054528 + * + */ +public class PolarChart implements ResultsPresenter { + + private final Chart chart = createPolarChartWidget(); + + private final Map dataByKeyStringAndAngle = new HashMap(); + + @Override + public Widget getWidget() { + return chart; + } + + /** + * Creates a polar diagram chart of the Line type. + */ + private Chart createPolarChartWidget() { + Chart polarSheetChart = new Chart().setType(Series.Type.LINE) + .setLinePlotOptions(new LinePlotOptions().setLineWidth(1)) + .setPolar(true).setHeight100().setWidth100(); + polarSheetChart.getYAxis().setMin(0); + return polarSheetChart; + } + + @Override + public void showResult(QueryResult result) { + resetChartData(); + Map results = result.getResults(); + if (!results.isEmpty()) { + for (Entry resultEntry : results.entrySet()) { + GroupKey key = resultEntry.getKey(); + StringBuilder keyStringBuilder = new StringBuilder(); + GroupKey currentKey = key; + while (currentKey.hasSubKey()) { + keyStringBuilder.append(currentKey.getMainKey()); + currentKey = currentKey.getSubKey(); + } + int angle = Integer.parseInt(currentKey.toString()); + String keyString = keyStringBuilder.toString(); + Series series = dataByKeyStringAndAngle.get(keyString); + if (series == null) { + series = chart.createSeries(); + dataByKeyStringAndAngle.put(keyString, series); + } + series.addPoint(angle, resultEntry.getValue()); + series.setName(keyString); + } + } + } + + private void resetChartData() { + dataByKeyStringAndAngle.clear(); + chart.removeAllSeries(); + } + + @Override + public void showError(String error) { + // TODO Auto-generated method stub + + } + + @Override + public void showError(String mainError, Iterable detailedErrors) { + // TODO Auto-generated method stub + + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/AngleOverDataSizeHistogramPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/AngleOverDataSizeHistogramPanel.java new file mode 100644 index 00000000000..187fc1727ed --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/AngleOverDataSizeHistogramPanel.java @@ -0,0 +1,130 @@ +package com.sap.sailing.gwt.ui.polarsheets; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.moxieapps.gwt.highcharts.client.AxisTitle; +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.Point; +import org.moxieapps.gwt.highcharts.client.Series; +import org.moxieapps.gwt.highcharts.client.Series.Type; +import org.moxieapps.gwt.highcharts.client.labels.XAxisLabels; +import org.moxieapps.gwt.highcharts.client.plotOptions.AreaPlotOptions; +import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; +import org.moxieapps.gwt.highcharts.client.plotOptions.PlotOptions.Stacking; + +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.DockLayoutPanel; +import com.sap.sailing.domain.common.PolarSheetsHistogramData; +import com.sap.sailing.gwt.ui.client.StringMessages; + +/** + * Shows the underlying distribution for any point in the polar diagrams + * of the {@link PolarSheetsChartPanel}. + * + * @author d054528 Frederik Petersen + * + */ +public class AngleOverDataSizeHistogramPanel extends DockLayoutPanel { + + private final Chart chart; + private final StringMessages stringMessages; + + private final Map> seriesForId; + + public AngleOverDataSizeHistogramPanel(StringMessages stringMessages) { + super(Unit.PX); + + this.seriesForId = new HashMap<>(); + this.stringMessages = stringMessages; + setSize("100%", "100%"); + + chart = createHistogramChart(); + chart.getElement().setAttribute("align", "top"); + add(chart); + } + + private Chart createHistogramChart() { + Chart histogramChart = new Chart().setType(Type.AREA).setHeight100().setWidth100(); + histogramChart.setChartTitleText(stringMessages.histogram()); + histogramChart.getYAxis().setMin(0).setAxisTitle(new AxisTitle().setText(stringMessages.numberOfDataPoints())); + histogramChart.getXAxis().setLabels(new XAxisLabels().setRotation(-90f).setY(10)) + .setAxisTitle(new AxisTitle().setText(stringMessages.beatAngle())); + histogramChart.setAreaPlotOptions(new AreaPlotOptions().setStacking(Stacking.NORMAL).setLineColor("#666666") + .setLineWidth(1).setMarker(new Marker().setLineWidth(1).setLineColor("#666666"))); + return histogramChart; + } + + public void addData(Map histogramDataPerAngle, String seriesId, String actualSeriesName) { + chart.setTitle(""); + Series series = chart.createSeries(); + + Point[] points = toPoints(histogramDataPerAngle); + series.setPoints(points); + series.setName(actualSeriesName); + chart.addSeries(series); + + if (!seriesForId.containsKey(seriesId)) { + seriesForId.put(seriesId, new HashSet()); + } + Set seriesSet = seriesForId.get(seriesId); + seriesSet.add(series); + } + + private Point[] toPoints(Map histogramDataPerAngle) { + Number[] xValues = new Number[360]; + Number[] yValues = new Number[360]; + for (int i = 0; i < 360; i++) { + xValues[i] = i; + yValues[i] = histogramDataPerAngle.get(i).getDataCount(); + } + + return toPoints(xValues, yValues); + } + + public Point[] toPoints(Number[] xValues, Number[] yValues) { + Point[] points = new Point[xValues.length]; + for (int i = 0; i < xValues.length; i++) { + points[i] = new Point(xValues[i], yValues[i]); + } + return points; + } + + @Override + protected void onLoad() { + Timer timer = new Timer() { + + @Override + public void run() { + chart.setSizeToMatchContainer(); + } + }; + timer.schedule(200); + super.onLoad(); + } + + @Override + public void onResize() { + chart.setSizeToMatchContainer(); + super.onResize(); + } + + public void removeSeries(String seriesId) { + for (Series series : seriesForId.get(seriesId)) { + chart.removeSeries(series); + } + seriesForId.remove(seriesId); + } + + public void removeAllSeries() { + chart.removeAllSeries(); + seriesForId.clear(); + } + + + + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/PolarSheetsChartPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/PolarSheetsChartPanel.java index db392c63865..f21fe20539f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/PolarSheetsChartPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/PolarSheetsChartPanel.java @@ -42,10 +42,13 @@ public class PolarSheetsChartPanel extends DockLayoutPanel { private PolarSheetGenerationSettings settings; private final Map polarSheetsDataMap; + + private final AngleOverDataSizeHistogramPanel angleOverDataSizeHistogramPanel; - public PolarSheetsChartPanel(StringMessages stringMessages) { + public PolarSheetsChartPanel(StringMessages stringMessages, AngleOverDataSizeHistogramPanel angleOverDataSizeHistogramPanel) { super(Unit.PCT); this.stringMessages = stringMessages; + this.angleOverDataSizeHistogramPanel = angleOverDataSizeHistogramPanel; polarSheetsDataMap = new HashMap(); setSize("100%", "100%"); chart = createPolarSheetChart(); @@ -90,13 +93,12 @@ public class PolarSheetsChartPanel extends DockLayoutPanel { * @param windSpeedLevel The id of the windspeed level in the windspeed steppings * @param windSpeed The actual windspeed */ - private void createSeriesForWindspeed(String name, int windSpeedLevel, int windSpeed) { + private void createSeriesForWindspeed(String name, int windSpeedLevel, String seriesName) { Series[] seriesPerWindSpeed = seriesMap.get(name); Number[] forEachDeg = initializeDataForNewSeries(); seriesPerWindSpeed[windSpeedLevel] = chart.createSeries().setPoints(forEachDeg); - String actualSeriesName = name + "-" + windSpeed; - seriesPerWindSpeed[windSpeedLevel].setName(actualSeriesName); - nameForSeries.put(seriesPerWindSpeed[windSpeedLevel],actualSeriesName); + seriesPerWindSpeed[windSpeedLevel].setName(seriesName); + nameForSeries.put(seriesPerWindSpeed[windSpeedLevel],seriesName); chart.addSeries(seriesPerWindSpeed[windSpeedLevel]); } @@ -117,14 +119,16 @@ public class PolarSheetsChartPanel extends DockLayoutPanel { if (seriesMap.containsKey(seriesId)) { for (int i = 0; i < stepCount; i++) { if (hasSufficientDataForWindspeed(result.getDataCountPerAngleForWindspeed(i))) { + String actualSeriesName = seriesId + "-" + result.getStepping().getRawStepping()[i]; if (seriesMap.get(seriesId)[i] == null) { - createSeriesForWindspeed(seriesId, i, result.getStepping().getRawStepping()[i]); + createSeriesForWindspeed(seriesId, i, actualSeriesName); } Series series = seriesMap.get(seriesId)[i]; - series.setPoints(result.getAveragedPolarDataByWindSpeed()[i], false); + //series.setPoints(result.getAveragedPolarDataByWindSpeed()[i], false); Point[] points = createPointsWithMarkerAlphaAccordingToDataCount(result, i); if (points != null) { series.setPoints(points); + angleOverDataSizeHistogramPanel.addData(result.getHistogramDataMap().get(i), seriesId, actualSeriesName); } } } @@ -154,14 +158,16 @@ public class PolarSheetsChartPanel extends DockLayoutPanel { Point[] points = new Point[360]; List dataCountList = Arrays.asList(result.getDataCountPerAngleForWindspeed(windspeed)); Integer max = Collections.max(dataCountList); - if (max <= 0) { + if (max < settings.getMinimumDataCountPerAngle()) { return null; } for (int i = 0; i < 360; i++) { if (result.getHistogramDataMap().get(windspeed) == null || result.getHistogramDataMap().get(windspeed).get(i) == null || result.getHistogramDataMap().get(windspeed).get(i).getConfidenceMeasure() < settings - .getMinimumConfidenceMeasure()) { + .getMinimumConfidenceMeasure() + || result.getHistogramDataMap().get(windspeed).get(i).getDataCount() < settings + .getMinimumDataCountPerAngle()) { points[i] = new Point(0); continue; } @@ -203,6 +209,7 @@ public class PolarSheetsChartPanel extends DockLayoutPanel { } seriesMap.remove(seriesId); polarSheetsDataMap.remove(seriesId); + angleOverDataSizeHistogramPanel.removeSeries(seriesId); } } @@ -213,6 +220,7 @@ public class PolarSheetsChartPanel extends DockLayoutPanel { chart.removeAllSeries(); seriesMap.clear(); polarSheetsDataMap.clear(); + angleOverDataSizeHistogramPanel.clear(); } /** diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/PolarSheetsPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/PolarSheetsPanel.java index 22972a4f375..b18f677dd8c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/PolarSheetsPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/PolarSheetsPanel.java @@ -12,9 +12,11 @@ import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; +import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.DockLayoutPanel; import com.google.gwt.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.ScrollPanel; import com.google.gwt.user.client.ui.SplitLayoutPanel; import com.google.gwt.user.client.ui.TextBox; @@ -24,6 +26,7 @@ import com.sap.sailing.domain.common.PolarSheetGenerationResponse; import com.sap.sailing.domain.common.PolarSheetGenerationSettings; import com.sap.sailing.domain.common.PolarSheetsData; import com.sap.sailing.domain.common.PolarSheetsHistogramData; +import com.sap.sailing.domain.common.PolarSheetsXYDiagramData; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.WindStepping; import com.sap.sailing.domain.common.impl.PolarSheetGenerationSettingsImpl; @@ -58,6 +61,7 @@ public class PolarSheetsPanel extends SplitLayoutPanel implements RaceSelectionC private List selectedRaces; private PolarSheetsChartPanel chartPanel; private PolarSheetsHistogramPanel histogramPanel; + private AngleOverDataSizeHistogramPanel angleOverDataSizeHistogramPanel; private Label polarSheetsGenerationLabel; private Label dataCountLabel; @@ -93,8 +97,10 @@ public class PolarSheetsPanel extends SplitLayoutPanel implements RaceSelectionC namingBox = new TextBox(); namingContainer.add(namingBox); leftPanel.add(namingContainer); + addBoatClassDiagramArea(leftPanel); DockLayoutPanel rightPanel = new DockLayoutPanel(Unit.PCT); - PolarSheetsChartPanel polarSheetsChartPanel = createPolarSheetsChartPanel(); + angleOverDataSizeHistogramPanel = new AngleOverDataSizeHistogramPanel(stringMessages); + PolarSheetsChartPanel polarSheetsChartPanel = createPolarSheetsChartPanel(angleOverDataSizeHistogramPanel); DockLayoutPanel polarChartAndControlPanel = new DockLayoutPanel(Unit.PCT); PolarChartControlPanel polarChartControlPanel = new PolarChartControlPanel(stringMessages, polarSheetsChartPanel); @@ -108,7 +114,11 @@ public class PolarSheetsPanel extends SplitLayoutPanel implements RaceSelectionC rightPanel.addNorth(polarChartAndControlPanel, 70); histogramPanel = new PolarSheetsHistogramPanel(stringMessages); histogramPanel.getElement().setAttribute("align", "top"); - rightPanel.addSouth(histogramPanel, 30); + //rightPanel.addSouth(histogramPanel, 30); + + + angleOverDataSizeHistogramPanel.getElement().setAttribute("align", "top"); + rightPanel.addSouth(angleOverDataSizeHistogramPanel, 30); add(rightPanel); @@ -119,6 +129,81 @@ public class PolarSheetsPanel extends SplitLayoutPanel implements RaceSelectionC chartPanel.setSettings(initialSettings); } + private void addBoatClassDiagramArea(VerticalPanel leftPanel) { + leftPanel.add(new Label(stringMessages.showBoatClassChartsLabel())); + final ListBox boatClassListBox = createBoatClassListBox(); + leftPanel.add(boatClassListBox); + Button showBoatClassDiagramButton = new Button(stringMessages.showDiagram()); + showBoatClassDiagramButton.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + chartPanel.showLoadingInfo(); + sailingService.showCachedPolarSheetForBoatClass( + boatClassListBox.getItemText(boatClassListBox.getSelectedIndex()), + new AsyncCallback() { + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(caught.getLocalizedMessage()); + } + + @Override + public void onSuccess(PolarSheetGenerationResponse result) { + String name = createNameForPolarSheet(result); + chartPanel.getPolarSheetsDataMap().put(result.getId(), result.getData()); + chartPanel.setData(name, result.getData()); + chartPanel.hideLoadingInfo(); + } + }); + } + }); + leftPanel.add(showBoatClassDiagramButton); + + Button showXYDiagramsButton = new Button(stringMessages.showXYDiagram()); + showXYDiagramsButton.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + sailingService.createXYDiagramForBoatClass( + boatClassListBox.getItemText(boatClassListBox.getSelectedIndex()), new AsyncCallback() { + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(caught.getLocalizedMessage()); + } + + @Override + public void onSuccess(PolarSheetsXYDiagramData result) { + PolarSheetsXYDiagramPopupPanel popupPanel = new PolarSheetsXYDiagramPopupPanel(stringMessages, result); + popupPanel.show(); + popupPanel.center(); + } + }); + } + }); + leftPanel.add(showXYDiagramsButton); + } + + private ListBox createBoatClassListBox() { + final ListBox boatClassListBox = new ListBox(); + sailingService.getBoatClassNamesWithPolarSheetsAvailable(new AsyncCallback>() { + + @Override + public void onFailure(Throwable caught) { + errorReporter.reportError(caught.getLocalizedMessage()); + } + + @Override + public void onSuccess(List result) { + for (String item : result) { + boatClassListBox.addItem(item); + } + } + }); + return boatClassListBox; + } + private void setEventListenersForPolarSheetChart() { PointSelectEventHandler pointSelectEventHandler = new PointSelectEventHandler() { @Override @@ -144,8 +229,8 @@ public class PolarSheetsPanel extends SplitLayoutPanel implements RaceSelectionC return polarSheetsGenerationStatusLabel; } - private PolarSheetsChartPanel createPolarSheetsChartPanel() { - chartPanel = new PolarSheetsChartPanel(stringMessages); + private PolarSheetsChartPanel createPolarSheetsChartPanel(AngleOverDataSizeHistogramPanel angleOverDataSizeHistogramPanel) { + chartPanel = new PolarSheetsChartPanel(stringMessages, angleOverDataSizeHistogramPanel); return chartPanel; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/PolarSheetsXYDiagramPopupPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/PolarSheetsXYDiagramPopupPanel.java new file mode 100644 index 00000000000..44626872dc8 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/PolarSheetsXYDiagramPopupPanel.java @@ -0,0 +1,97 @@ +package com.sap.sailing.gwt.ui.polarsheets; + +import java.util.List; + +import org.moxieapps.gwt.highcharts.client.AxisTitle; +import org.moxieapps.gwt.highcharts.client.Chart; +import org.moxieapps.gwt.highcharts.client.PlotLine; +import org.moxieapps.gwt.highcharts.client.Point; +import org.moxieapps.gwt.highcharts.client.Series; +import org.moxieapps.gwt.highcharts.client.Series.Type; +import org.moxieapps.gwt.highcharts.client.plotOptions.Marker; +import org.moxieapps.gwt.highcharts.client.plotOptions.SplinePlotOptions; + +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.user.client.ui.Button; +import com.google.gwt.user.client.ui.DialogBox; +import com.google.gwt.user.client.ui.VerticalPanel; +import com.sap.sailing.domain.common.PolarSheetsXYDiagramData; +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sse.common.Util.Pair; + +public class PolarSheetsXYDiagramPopupPanel extends DialogBox { + + + private final Chart chart; + private final StringMessages stringMessages; + + public PolarSheetsXYDiagramPopupPanel(StringMessages stringMessages, PolarSheetsXYDiagramData result) { + this.stringMessages = stringMessages; + chart = createChart(); + VerticalPanel containerPanel = new VerticalPanel(); + this.add(containerPanel); + containerPanel.add(chart); + + + Button closeButton = new Button(stringMessages.close()); + closeButton.addClickHandler(new ClickHandler() { + + @Override + public void onClick(ClickEvent event) { + PolarSheetsXYDiagramPopupPanel.this.hide(false); + } + }); + containerPanel.add(closeButton); + + Point[] pointsForUpwindStarboardAverageAngle = toPointArray(result.getPointsForUpwindStarboardAverageAngle()); + Series angleSeries = chart.createSeries(); + angleSeries.setPoints(pointsForUpwindStarboardAverageAngle); + //chart.addSeries(angleSeries); + + Point[] pointsForUpwindStarboardAverageSpeed = toPointArray(result.getPointsForUpwindStarboardAverageSpeed()); + Series speedSeries = chart.createSeries(); + speedSeries.setName("Upwind Starboard Speed - LinearRegressions"); + speedSeries.setPoints(pointsForUpwindStarboardAverageSpeed); + chart.addSeries(speedSeries); + + Point[] pointsForUpwindStarboardAverageSpeedMovingAverage = toPointArray(result.getPointsForUpwindStarboardAverageSpeedMovingAverage()); + Series speedSeriesMovingAverage = chart.createSeries(); + speedSeriesMovingAverage.setName("Upwind Starboard Speed - MovingAverage"); + speedSeriesMovingAverage.setPoints(pointsForUpwindStarboardAverageSpeedMovingAverage); + chart.addSeries(speedSeriesMovingAverage); + + Point[] pointsForUpwindStarboardAverageConfidence = toPointArray(result.getPointsForUpwindStarboardAverageConfidence()); + Series confidenceSeriesMovingAverage = chart + .createSeries() + .setYAxis(1) + .setType(Series.Type.SPLINE) + .setPlotOptions( + new SplinePlotOptions().setColor("#AA4643").setMarker(new Marker().setEnabled(false)) + .setDashStyle(PlotLine.DashStyle.SHORT_DOT)); + confidenceSeriesMovingAverage.setName("Upwind Starboard Confidence"); + confidenceSeriesMovingAverage.setPoints(pointsForUpwindStarboardAverageConfidence); + chart.addSeries(confidenceSeriesMovingAverage); + } + + private Point[] toPointArray(List> pointsForUpwindStarboardAverageAngle) { + Point[] points = new Point[pointsForUpwindStarboardAverageAngle.size()]; + int i = 0; + for (Pair point : pointsForUpwindStarboardAverageAngle) { + points[i] = new Point(point.getA(), point.getB()); + i++; + } + return points; + } + + private Chart createChart() { + Chart chart = new Chart().setType(Type.LINE); + chart.setWidth(1000); + chart.setTitle(stringMessages.xyDiagram()); + chart.getXAxis().setAxisTitle(new AxisTitle().setText(stringMessages.windSpeed())); + chart.getYAxis(0).setAxisTitle(new AxisTitle().setText(stringMessages.boatSpeed())); + chart.getYAxis(1).setAxisTitle(new AxisTitle().setText(stringMessages.confidence())); + return chart; + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/WindSteppingConfiguratorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/WindSteppingConfiguratorPanel.java index a82b22b408b..508268c748a 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/WindSteppingConfiguratorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/polarsheets/WindSteppingConfiguratorPanel.java @@ -27,9 +27,9 @@ public class WindSteppingConfiguratorPanel extends HorizontalPanel { public WindSteppingConfiguratorPanel(WindSteppingWithMaxDistance windStepping) { setupPlusAndMinusButtons(); - Integer[] levels = windStepping.getRawStepping(); + Double[] levels = windStepping.getRawStepping(); for (int i = 0; i < levels.length; i++) { - TextBox textBox = createSingleBox(levels[i]); + TextBox textBox = createSingleBox((int) Math.round(levels[i])); textBoxes.add(textBox); } updateTextBoxes(); @@ -69,12 +69,12 @@ public class WindSteppingConfiguratorPanel extends HorizontalPanel { } public WindSteppingWithMaxDistance getStepping(double maxDistance) { - List levelList = new ArrayList(); + List levelList = new ArrayList(); for (TextBox box : textBoxes) { - levelList.add(Integer.parseInt(box.getValue())); + levelList.add(new Double(Integer.parseInt(box.getValue()))); } Collections.sort(levelList); - Integer[] levels = levelList.toArray(new Integer[levelList.size()]); + Double[] levels = levelList.toArray(new Double[levelList.size()]); return new WindSteppingWithMaxDistance(levels, maxDistance); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java index 65e5ca33126..bec7fd090b2 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java @@ -139,12 +139,12 @@ public class SideBySideComponentViewer implements ComponentViewer { splitLayoutPanel.insert(panelForMapAndHorizontalToggleButtons, rightComponent, Direction.CENTER, 0); // add additional toggle buttons panel that currently only contains the video button - List>> additionalVerticalButtons = new ArrayList>>(); - additionalVerticalButtons.add(new Pair>(mediaSelectionButton, - mediaPlayerManagerComponent)); + List> additionalVerticalButtons = new ArrayList>(); + additionalVerticalButtons.add(new Pair(mediaSelectionButton, + mediaPlayerManagerComponent.getDependentCssClassName())); if (/* TODO check for correct role; was: user != null */ true) { - additionalVerticalButtons.add(new Pair>(mediaManagementButton, - mediaPlayerManagerComponent)); + additionalVerticalButtons.add(new Pair(mediaManagementButton, + "managemedia")); } // ensure that toggle buttons are positioned right diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java index dd1a082b424..3794acc3cb5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/TouchSplitLayoutPanel.java @@ -593,7 +593,7 @@ public class TouchSplitLayoutPanel extends DockLayoutPanel { * outside of the horizontal splitter panel. In most cases one will create such a panel * on top of the widget that has position {@link Direction#CENTER}. */ - public void lastComponentHasBeenAdded(final SideBySideComponentViewer cm, AbsolutePanel panelForHorizontalButtons, List>> additionalVerticalButtons) { + public void lastComponentHasBeenAdded(final SideBySideComponentViewer cm, AbsolutePanel panelForHorizontalButtons, List> additionalVerticalButtonsAndStyles) { WidgetCollection splitterChildren = super.getChildren(); HSplitter lastHorizontalSplitter = null; VSplitter lastVerticalSplitter = null; @@ -613,7 +613,7 @@ public class TouchSplitLayoutPanel extends DockLayoutPanel { if (lastVerticalSplitter != null) { Panel panel = createToggleButtonPanel(allVerticalSplitters, "gwt-SplitLayoutPanel-NorthSouthToggleButton-Panel", "gwt-SplitLayoutPanel-NorthSouthToggleButton", - cm, additionalVerticalButtons); + cm, additionalVerticalButtonsAndStyles); lastVerticalSplitter.addToogleButtons(panel); lastVerticalSplitter.setVisible(true); lastVerticalSplitter.setDraggerVisible(false); @@ -633,16 +633,16 @@ public class TouchSplitLayoutPanel extends DockLayoutPanel { * {@link Splitter}s including the {@link Component} and {@link Widget}. */ private Panel createToggleButtonPanel(List splitters, String panelStyleName, String buttonStyleName, - final SideBySideComponentViewer componentViewer, List>> additionalButtons) { + final SideBySideComponentViewer componentViewer, List> additionalButtonsAndStyles) { FlowPanel buttonFlowPanel = new FlowPanel(); buttonFlowPanel.setStyleName(panelStyleName); - if (additionalButtons != null) { - for (Pair> buttonAndComponentPair : additionalButtons) { + if (additionalButtonsAndStyles != null) { + for (Pair buttonAndComponentPair : additionalButtonsAndStyles) { Button button = buttonAndComponentPair.getA(); button.setStyleName(buttonStyleName); - button.addStyleDependentName("Closed-"+buttonAndComponentPair.getB().getDependentCssClassName()); + button.addStyleDependentName("Closed-"+buttonAndComponentPair.getB()); if (Document.get().getClientWidth() <= 1024) { - button.addStyleDependentName("Small-"+buttonAndComponentPair.getB().getDependentCssClassName()); + button.addStyleDependentName("Small-"+buttonAndComponentPair.getB()); } buttonFlowPanel.add(button); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index a4c00c2ccd1..70bc3ea1955 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -102,6 +102,7 @@ import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.RemoteSailingServerReference; import com.sap.sailing.domain.base.Series; import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.base.configuration.DeviceConfiguration; import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; @@ -141,6 +142,7 @@ import com.sap.sailing.domain.common.PassingInstruction; import com.sap.sailing.domain.common.PolarSheetGenerationResponse; import com.sap.sailing.domain.common.PolarSheetGenerationSettings; import com.sap.sailing.domain.common.PolarSheetsData; +import com.sap.sailing.domain.common.PolarSheetsXYDiagramData; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.RaceFetcher; import com.sap.sailing.domain.common.RaceIdentifier; @@ -185,6 +187,7 @@ import com.sap.sailing.domain.common.impl.KnotSpeedImpl; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.MeterDistance; import com.sap.sailing.domain.common.impl.PolarSheetGenerationResponseImpl; +import com.sap.sailing.domain.common.impl.PolarSheetsXYDiagramDataImpl; import com.sap.sailing.domain.common.impl.TimeRangeImpl; import com.sap.sailing.domain.common.impl.WindSourceImpl; import com.sap.sailing.domain.common.media.MediaTrack; @@ -213,7 +216,6 @@ import com.sap.sailing.domain.leaderboard.caching.LiveLeaderboardUpdater; import com.sap.sailing.domain.persistence.DomainObjectFactory; import com.sap.sailing.domain.persistence.MongoObjectFactory; import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory; -import com.sap.sailing.domain.polarsheets.PolarSheetGenerationWorker; import com.sap.sailing.domain.racelog.RaceStateOfSameDayHelper; import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapter; import com.sap.sailing.domain.racelogtracking.RaceLogTrackingAdapterFactory; @@ -330,6 +332,8 @@ import com.sap.sailing.manage2sail.EventResultDescriptor; import com.sap.sailing.manage2sail.Manage2SailEventResultsParserImpl; import com.sap.sailing.manage2sail.RaceResultDescriptor; import com.sap.sailing.manage2sail.RegattaResultDescriptor; +import com.sap.sailing.polars.PolarDataService; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; import com.sap.sailing.resultimport.ResultUrlProvider; import com.sap.sailing.resultimport.ResultUrlRegistry; import com.sap.sailing.server.RacingEventService; @@ -478,7 +482,6 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S private final SwissTimingReplayService swissTimingReplayService; private final QuickRanksLiveCache quickRanksLiveCache; - public SailingServiceImpl() { BundleContext context = Activator.getDefault(); Activator activator = Activator.getInstance(); @@ -3898,15 +3901,33 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S for (RegattaAndRaceIdentifier race : selectedRaces) { trackedRaces.add(service.getTrackedRace(race)); } - PolarSheetGenerationWorker genWorker = new PolarSheetGenerationWorker(trackedRaces, settings, executor); - genWorker.startPolarSheetGeneration(); if (name == null || name.isEmpty()) { name = getCommonBoatClass(trackedRaces); } - PolarSheetsData result = genWorker.get(); + PolarDataService polarDataService = service.getPolarDataService(); + PolarSheetsData result = polarDataService.generatePolarSheet(trackedRaces, settings, executor); return new PolarSheetGenerationResponseImpl(id, name, result); } + @Override + public List getBoatClassNamesWithPolarSheetsAvailable() { + Set boatClasses = getService().getPolarDataService().getAllBoatClassesWithPolarSheetsAvailable(); + List names = new ArrayList(); + for (BoatClass boatClass : boatClasses) { + names.add(boatClass.getName()); + } + return names; + } + + @Override + public PolarSheetGenerationResponse showCachedPolarSheetForBoatClass(String boatClassName) { + BoatClass boatClass = getService().getBaseDomainFactory().getOrCreateBoatClass(boatClassName); + PolarSheetsData data = getService().getPolarDataService().getPolarSheetForBoatClass(boatClass); + String name = boatClassName + "_OVERALL"; + String id = name; + return new PolarSheetGenerationResponseImpl(id, name, data); + } + private String getCommonBoatClass(Set trackedRaces) { BoatClass boatClass = null; for (TrackedRace race : trackedRaces) { @@ -5221,4 +5242,44 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S } return null; } -} + + @Override + public PolarSheetsXYDiagramData createXYDiagramForBoatClass(String boatClassName) { + BoatClass boatClass = getService().getBaseDomainFactory().getOrCreateBoatClass(boatClassName); + List> pointsForUpwindStarboardAverageSpeed = new ArrayList>(); + List> pointsForUpwindStarboardAverageAngle = new ArrayList>(); + List> pointsForUpwindStarboardAverageSpeedMovingAverage = new ArrayList>(); + List> pointsForUpwindStarboardAverageAngleMovingAverage = new ArrayList>(); + List> pointsForUpwindStarboardAverageConfidence = new ArrayList>(); + for (double windInKnots = 0.1; windInKnots < 30; windInKnots = windInKnots + 0.1) { + try { + SpeedWithBearingWithConfidence averageUpwindStarboard = getService().getPolarDataService() + .getAverageSpeedWithBearing(boatClass, new KnotSpeedImpl(windInKnots), LegType.UPWIND, + Tack.STARBOARD, true); + pointsForUpwindStarboardAverageSpeed.add(new Pair(windInKnots, averageUpwindStarboard + .getObject().getKnots())); + pointsForUpwindStarboardAverageAngle.add(new Pair(windInKnots, averageUpwindStarboard + .getObject().getBearing().getDegrees())); + + pointsForUpwindStarboardAverageConfidence.add(new Pair(windInKnots, + averageUpwindStarboard.getConfidence())); + + SpeedWithBearingWithConfidence averageUpwindStarboardMovingAverage = getService().getPolarDataService() + .getAverageSpeedWithBearing(boatClass, new KnotSpeedImpl(windInKnots), LegType.UPWIND, + Tack.STARBOARD, false); + pointsForUpwindStarboardAverageSpeedMovingAverage.add(new Pair(windInKnots, + averageUpwindStarboardMovingAverage.getObject().getKnots())); + pointsForUpwindStarboardAverageAngleMovingAverage.add(new Pair(windInKnots, + averageUpwindStarboardMovingAverage.getObject().getBearing().getDegrees())); + + } catch (NotEnoughDataHasBeenAddedException e) { + // Do not add a point to the result + } + } + PolarSheetsXYDiagramData data = new PolarSheetsXYDiagramDataImpl(pointsForUpwindStarboardAverageAngle, + pointsForUpwindStarboardAverageSpeed, pointsForUpwindStarboardAverageAngleMovingAverage, + pointsForUpwindStarboardAverageSpeedMovingAverage, pointsForUpwindStarboardAverageConfidence); + + return data; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventBaseDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventBaseDTO.java index 16ae5c9cbba..7a65e724dbd 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventBaseDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/EventBaseDTO.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.Map; import java.util.UUID; -import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.rpc.IsSerializable; import com.sap.sailing.domain.common.ImageSize; import com.sap.sailing.domain.common.dto.NamedDTO; @@ -210,8 +209,6 @@ public class EventBaseDTO extends NamedDTO implements IsSerializable { if (imageSize != null && imageSize.getHeight() > MINIMUM_IMAGE_HEIGHT_FOR_SAILING_PHOTOGRAPHY_IN_PIXELS && !candidateImageUrl.toLowerCase().contains(STAGE_IMAGE_URL_SUBSTRING_INDICATOR_CASE_INSENSITIVE)) { acceptedImages.add(candidateImageUrl); - } else { - GWT.log("Rejected: " + candidateImageUrl); } } return acceptedImages; diff --git a/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF index 0a59edb2280..9e12ea444e6 100755 --- a/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.mongodb.test/META-INF/MANIFEST.MF @@ -1,33 +1,34 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Test -Bundle-SymbolicName: com.sap.sailing.mongodb.test -Bundle-Version: 1.0.0.qualifier -Bundle-Vendor: SAP -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Fragment-Host: com.sap.sse.mongodb -Require-Bundle: com.sap.sailing.domain, - com.sap.sailing.domain.shared.android, - com.sap.sailing.domain.test, - com.tractrac.clientmodule;bundle-version="2.0.0", - com.sap.sailing.domain.tractracadapter, - com.sap.sailing.domain.persistence, - com.sap.sailing.server, - com.sap.sailing.expeditionconnector, - org.json.simple;bundle-version="1.1.0", - com.sap.sailing.domain.swisstimingadapter, - org.objenesis;bundle-version="1.3.0", - org.junit4;bundle-version="4.8.2", - org.hamcrest;bundle-version="1.1.0", - org.mockito.mockito-core;bundle-version="1.9.5", - com.sap.sailing.domain.racelogtrackingadapter.testsupport, - com.sap.sailing.server.gateway;bundle-version="1.0.0", - com.sap.sse.common, - com.sap.sse.operationaltransformation, - com.sap.sse.replication, - com.sap.sse -Import-Package: com.sap.sailing.domain.common, - com.sap.sailing.domain.common.impl, - com.sap.sailing.domain.common.racelog, - com.sap.sailing.domain.common.racelog.tracking - +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Test +Bundle-SymbolicName: com.sap.sailing.mongodb.test +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Fragment-Host: com.sap.sse.mongodb +Require-Bundle: com.sap.sailing.domain, + com.sap.sailing.domain.shared.android, + com.sap.sailing.domain.test, + com.tractrac.clientmodule;bundle-version="2.0.0", + com.sap.sailing.domain.tractracadapter, + com.sap.sailing.domain.persistence, + com.sap.sailing.server, + com.sap.sailing.expeditionconnector, + org.json.simple;bundle-version="1.1.0", + com.sap.sailing.domain.swisstimingadapter, + org.objenesis;bundle-version="1.3.0", + org.junit4;bundle-version="4.8.2", + org.hamcrest;bundle-version="1.1.0", + org.mockito.mockito-core;bundle-version="1.9.5", + com.sap.sailing.polars, + com.sap.sailing.domain.racelogtrackingadapter.testsupport, + com.sap.sailing.server.gateway, + com.sap.sse.common, + com.sap.sse.operationaltransformation, + com.sap.sse.replication, + com.sap.sse +Import-Package: com.sap.sailing.domain.common, + com.sap.sailing.domain.common.impl, + com.sap.sailing.domain.common.racelog, + com.sap.sailing.domain.common.racelog.tracking + diff --git a/java/com.sap.sailing.polars.test/.classpath b/java/com.sap.sailing.polars.test/.classpath new file mode 100644 index 00000000000..e334ecd9727 --- /dev/null +++ b/java/com.sap.sailing.polars.test/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/java/com.sap.sailing.polars.test/.project b/java/com.sap.sailing.polars.test/.project new file mode 100644 index 00000000000..7980f4925e6 --- /dev/null +++ b/java/com.sap.sailing.polars.test/.project @@ -0,0 +1,28 @@ + + + com.sap.sailing.polars.test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/java/com.sap.sailing.polars.test/.settings/org.eclipse.jdt.core.prefs b/java/com.sap.sailing.polars.test/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..a698e59674f --- /dev/null +++ b/java/com.sap.sailing.polars.test/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/java/com.sap.sailing.polars.test/.settings/org.eclipse.pde.core.prefs b/java/com.sap.sailing.polars.test/.settings/org.eclipse.pde.core.prefs new file mode 100644 index 00000000000..f29e940a005 --- /dev/null +++ b/java/com.sap.sailing.polars.test/.settings/org.eclipse.pde.core.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +pluginProject.extensions=false +resolve.requirebundle=false diff --git a/java/com.sap.sailing.polars.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.polars.test/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..597c4f9acd3 --- /dev/null +++ b/java/com.sap.sailing.polars.test/META-INF/MANIFEST.MF @@ -0,0 +1,14 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: PolarsTest +Bundle-SymbolicName: com.sap.sailing.polars.test +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Fragment-Host: com.sap.sailing.polars +Import-Package: com.sap.sailing.domain.test.mock, + com.sap.sse.common, + com.sap.sse.common.impl, + org.junit;version="4.8.2" +Require-Bundle: org.mockito.mockito-core;bundle-version="1.9.5", + org.hamcrest;bundle-version="1.1.0" diff --git a/java/com.sap.sailing.polars.test/build.properties b/java/com.sap.sailing.polars.test/build.properties new file mode 100644 index 00000000000..34d2e4d2dad --- /dev/null +++ b/java/com.sap.sailing.polars.test/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/caching/test/PolarFixCacheRaceIntervalTest.java b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/caching/test/PolarFixCacheRaceIntervalTest.java new file mode 100644 index 00000000000..f3ee8c2b40f --- /dev/null +++ b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/caching/test/PolarFixCacheRaceIntervalTest.java @@ -0,0 +1,90 @@ +package com.sap.sailing.polars.caching.test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.polars.caching.PolarFixCacheRaceInterval; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class PolarFixCacheRaceIntervalTest { + + @Test + public void testIntervalJoin() { + TrackedRace mockedTrackedRace1 = mock(TrackedRace.class); + TrackedRace mockedTrackedRace2 = mock(TrackedRace.class); + + Competitor mockedCompetitor1 = mock(Competitor.class); + Competitor mockedCompetitor2 = mock(Competitor.class); + + Calendar calender = Calendar.getInstance(); + calender.set(2014, 3, 17, 16, 0, 0); + TimePoint timePoint1 = new MillisecondsTimePoint(calender.getTime()); + calender.set(2014, 3, 17, 16, 10, 0); + TimePoint timePoint2 = new MillisecondsTimePoint(calender.getTime()); + + PolarFixCacheRaceInterval interval1 = createInterval1( + mockedTrackedRace1, mockedTrackedRace2, mockedCompetitor1, + mockedCompetitor2, timePoint1, timePoint2); + + PolarFixCacheRaceInterval interval2 = createInterval2( + mockedTrackedRace1, mockedTrackedRace2, mockedCompetitor1, + timePoint2); + PolarFixCacheRaceInterval joined = interval1.join(interval2); + + Map>> joinedMap = joined + .getCompetitorAndTimepointsForRace(); + + Map> explicitIntervalForRace1 = joinedMap.get(mockedTrackedRace1); + assertThat(explicitIntervalForRace1, notNullValue(Map.class)); + Pair explicitIntervalForCompetitor1 = explicitIntervalForRace1.get(mockedCompetitor1); + assertThat(explicitIntervalForCompetitor1, notNullValue(Pair.class)); + + TimePoint start1 = explicitIntervalForCompetitor1.getA(); + TimePoint end1 = explicitIntervalForCompetitor1.getB(); + + assertThat(start1, is(timePoint1)); + assertThat(end1, is(timePoint2)); + + } + + private PolarFixCacheRaceInterval createInterval1( + TrackedRace mockedTrackedRace1, TrackedRace mockedTrackedRace2, + Competitor mockedCompetitor1, Competitor mockedCompetitor2, + TimePoint timePoint1, TimePoint timePoint2) { + Map>> competitorAndTimepointsForRace = new HashMap>>(); + HashMap> mapForRace1 = new HashMap>(); + mapForRace1.put(mockedCompetitor1, new Pair(timePoint1, timePoint1)); + competitorAndTimepointsForRace.put(mockedTrackedRace1, mapForRace1); + HashMap> mapForRace2 = new HashMap>(); + mapForRace2.put(mockedCompetitor2, new Pair(timePoint1, timePoint2)); + competitorAndTimepointsForRace.put(mockedTrackedRace2, mapForRace2); + PolarFixCacheRaceInterval interval1 = new PolarFixCacheRaceInterval( + competitorAndTimepointsForRace); + return interval1; + } + + private PolarFixCacheRaceInterval createInterval2( + TrackedRace mockedTrackedRace1, TrackedRace mockedTrackedRace2, + Competitor mockedCompetitor1, TimePoint timePoint2) { + Map>> competitorAndTimepointsForRace = new HashMap>>(); + HashMap> mapForRace1 = new HashMap>(); + mapForRace1.put(mockedCompetitor1, new Pair( + timePoint2, timePoint2)); + competitorAndTimepointsForRace.put(mockedTrackedRace1, mapForRace1); + PolarFixCacheRaceInterval interval2 = new PolarFixCacheRaceInterval( + competitorAndTimepointsForRace); + return interval2; + } +} diff --git a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/clusters/test/SpeedClusterGroupTest.java b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/clusters/test/SpeedClusterGroupTest.java new file mode 100644 index 00000000000..b1e3e35aa02 --- /dev/null +++ b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/clusters/test/SpeedClusterGroupTest.java @@ -0,0 +1,72 @@ +package com.sap.sailing.polars.clusters.test; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +import com.sap.sailing.polars.clusters.SpeedClusterGroup; +import com.sap.sse.datamining.data.Cluster; + +public class SpeedClusterGroupTest { + + @Test + public void testWithZeroLevels() { + double[] levelMidsInKnots = {}; + SpeedClusterGroup group = new SpeedClusterGroup("test", levelMidsInKnots, 4); + assertThat(group.getClusterFor(new KnotSpeedImpl(0)), nullValue()); + assertThat(group.getClusterFor(new KnotSpeedImpl(6)), nullValue()); + } + + @Test + public void testWithOneLevelReachingZero() { + double[] levelMidsInKnots = { 4 }; + SpeedClusterGroup group = new SpeedClusterGroup("test", levelMidsInKnots, 4); + Cluster clusterForZero = group.getClusterFor(new KnotSpeedImpl(0)); + assertThat(clusterForZero, notNullValue()); + Cluster clusterForSix = group.getClusterFor(new KnotSpeedImpl(6)); + assertThat(clusterForSix, notNullValue()); + assertThat(clusterForSix == clusterForZero, equalTo(true)); + assertThat(group.getClusterFor(new KnotSpeedImpl(9)), nullValue()); + } + + @Test + public void testWithOneLevelNotReachingZero() { + double[] levelMidsInKnots = { 7 }; + SpeedClusterGroup group = new SpeedClusterGroup("test", levelMidsInKnots, 4); + Cluster clusterForNine = group.getClusterFor(new KnotSpeedImpl(9)); + assertThat(clusterForNine, notNullValue()); + Cluster clusterForSix = group.getClusterFor(new KnotSpeedImpl(6)); + assertThat(clusterForSix, notNullValue()); + assertThat(clusterForSix == clusterForNine, equalTo(true)); + assertThat(group.getClusterFor(new KnotSpeedImpl(0)), nullValue()); + } + + @Test + public void testWithMultipleLevelsWithNoEmptyRoomInbetween() { + double[] levelMidsInKnots = { 4, 6, 8, 10, 12, 15 }; + SpeedClusterGroup group = new SpeedClusterGroup("test", levelMidsInKnots, 4); + Cluster clusterForFourteen = group.getClusterFor(new KnotSpeedImpl(14)); + assertThat(clusterForFourteen, notNullValue()); + Cluster clusterForThirteen = group.getClusterFor(new KnotSpeedImpl(13)); + assertThat(clusterForThirteen, notNullValue()); + assertThat(clusterForThirteen == clusterForFourteen, equalTo(false)); + assertThat(group.getClusterFor(new KnotSpeedImpl(20)), nullValue()); + } + + @Test + public void testWithMultipleLevelsWithRoomInbetween() { + double[] levelMidsInKnots = { 4, 6, 15 }; + SpeedClusterGroup group = new SpeedClusterGroup("test", levelMidsInKnots, 4); + Cluster clusterForFourteen = group.getClusterFor(new KnotSpeedImpl(14)); + assertThat(clusterForFourteen, notNullValue()); + Cluster clusterForSeven = group.getClusterFor(new KnotSpeedImpl(7)); + assertThat(clusterForSeven, notNullValue()); + assertThat(clusterForSeven == clusterForFourteen, equalTo(false)); + assertThat(group.getClusterFor(new KnotSpeedImpl(10.5)), nullValue()); + } +} diff --git a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/mining/test/PolarDataMinerTest.java b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/mining/test/PolarDataMinerTest.java new file mode 100644 index 00000000000..2d81c4d20b1 --- /dev/null +++ b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/mining/test/PolarDataMinerTest.java @@ -0,0 +1,209 @@ +package com.sap.sailing.polars.mining.test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Calendar; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.TimeoutException; + +import org.junit.Test; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.Wind; +import com.sap.sailing.domain.tracking.WindWithConfidence; +import com.sap.sailing.domain.tracking.impl.DynamicGPSFixMovingTrackImpl; +import com.sap.sailing.domain.tracking.impl.WindImpl; +import com.sap.sailing.domain.tracking.impl.WindWithConfidenceImpl; +import com.sap.sailing.polars.mining.PolarDataMiner; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class PolarDataMinerTest { + + private static final int MILLISECONDS_OVER_WHICH_TO_AVERAGE_SPEED = 30; + private static final double EPSILON = 1E-4; + + @Test + public void testGrouping() throws InterruptedException, TimeoutException, NoSuchMethodException, + NotEnoughDataHasBeenAddedException { + PolarDataMiner miner = new PolarDataMiner(); + + BoatClass mockedBoatClass = mock(BoatClass.class); + + GPSFixMoving fix1_1 = createMockedFix(13, 00, 54.431952, 10.186767, 45, 10.5); + GPSFixMoving fix1_2 = createMockedFix(13, 30, 54.485034, 10.538303, 44.8, 10.6); + Competitor competitor1 = mock(Competitor.class); + + GPSFixMoving fix2_1 = createMockedFix(13, 00, 54.443942, 10.172739, 42.2, 10); + GPSFixMoving fix2_2 = createMockedFix(13, 30, 54.425394, 10.177404, 41.8, 10.1); + Competitor competitor2 = mock(Competitor.class); + + GPSFixMoving fix3_1 = createMockedFix(13, 45, 54.873740, 10.193648, 43.2, 10); + Competitor competitor3 = mock(Competitor.class); + + Map> fixesPerCompetitor = new HashMap>(); + + Set setForCompetitor1 = new HashSet(); + setForCompetitor1.add(fix1_1); + setForCompetitor1.add(fix1_2); + fixesPerCompetitor.put(competitor1, setForCompetitor1); + + Set setForCompetitor2 = new HashSet(); + setForCompetitor2.add(fix2_1); + setForCompetitor2.add(fix2_2); + fixesPerCompetitor.put(competitor2, setForCompetitor2); + + Set setForCompetitor3 = new HashSet(); + setForCompetitor2.add(fix3_1); + fixesPerCompetitor.put(competitor3, setForCompetitor3); + + + TrackedRace trackedRace = createMockedTrackedRace(fixesPerCompetitor, mockedBoatClass); + + for (Entry> entry : fixesPerCompetitor.entrySet()) { + Competitor competitor = entry.getKey(); + Set fixSet = entry.getValue(); + for (GPSFixMoving fix : fixSet) { + miner.addFix(fix, competitor, trackedRace); + } + } + + int millisLeft = 500000; + while (miner.isCurrentlyActiveAndOrHasQueue() && millisLeft > 0) { + Thread.sleep(100); + millisLeft = millisLeft - 100; + if (miner.isCurrentlyActiveAndOrHasQueue() && millisLeft <= 0) { + throw new TimeoutException(); + } + } + + SpeedWithConfidence estimatedSpeed1 = miner.estimateBoatSpeed(mockedBoatClass, new KnotSpeedImpl(15), + new DegreeBearingImpl(44.9), true); + assertThat(estimatedSpeed1, is(notNullValue())); + assertThat(estimatedSpeed1.getObject().getKnots(), is(closeTo(10.55, EPSILON))); + + SpeedWithConfidence estimatedSpeed2 = miner.estimateBoatSpeed(mockedBoatClass, new KnotSpeedImpl(15), + new DegreeBearingImpl(42), true); + assertThat(estimatedSpeed2, is(notNullValue())); + assertThat(estimatedSpeed2.getObject().getKnots(), is(closeTo(10.05, EPSILON))); + + SpeedWithConfidence estimatedSpeed3 = miner.estimateBoatSpeed(mockedBoatClass, new KnotSpeedImpl(15), + new DegreeBearingImpl(42.8), true); + assertThat(estimatedSpeed3, is(notNullValue())); + assertThat(estimatedSpeed3.getObject().getKnots(), is(closeTo(10, EPSILON))); + } + + private GPSFixMoving createMockedFix(int hour, int minute, double lat, double lng, double bearingRaw, double speed) { + GPSFixMoving fix = mock(GPSFixMoving.class); + when(fix.getPosition()).thenReturn(new DegreePosition(lat, lng)); + Calendar cal = Calendar.getInstance(); + cal.set(2014, 4, 3, hour, minute); + TimePoint fixTimePoint = new MillisecondsTimePoint(cal.getTime()); + when(fix.getTimePoint()).thenReturn(fixTimePoint); + Bearing bearing = new DegreeBearingImpl(bearingRaw); + SpeedWithBearing speedWithBearing = new KnotSpeedWithBearingImpl(speed, bearing); + when(fix.getSpeed()).thenReturn(speedWithBearing); + return fix; + } + + private TrackedRace createMockedTrackedRace(Map> fixesPerCompetitor, + BoatClass mockedBoatClass) { + TrackedRace trackedRace = mock(TrackedRace.class); + + for (Entry> competitorEntry : fixesPerCompetitor.entrySet()) { + Competitor competitor = competitorEntry.getKey(); + DynamicGPSFixTrack track = new DynamicGPSFixMovingTrackImpl( + competitor, MILLISECONDS_OVER_WHICH_TO_AVERAGE_SPEED); + when(trackedRace.getTrack(competitor)).thenReturn(track); + for (GPSFixMoving fix : competitorEntry.getValue()) { + track.add(fix); + } + } + + RaceDefinition mockedRaceDefinition = createMockedRaceDefinition(); + when(mockedRaceDefinition.getBoatClass()).thenReturn(mockedBoatClass); + when(trackedRace.getRace()).thenReturn(mockedRaceDefinition); + + for (Competitor competitor : fixesPerCompetitor.keySet()) { + MarkPassing markpassing = createMockedStartMarkPassing(); + when(trackedRace.getMarkPassing(eq(competitor), any(Waypoint.class))).thenReturn(markpassing); + } + + Calendar cal = Calendar.getInstance(); + cal.set(2014, 4, 3, 12, 00); + TimePoint startOfRace = new MillisecondsTimePoint(cal.getTime()); + + cal.set(2014, 4, 3, 15, 00); + TimePoint endOfRace = new MillisecondsTimePoint(cal.getTime()); + + when(trackedRace.getStartOfRace()).thenReturn(startOfRace); + when(trackedRace.getEndOfRace()).thenReturn(endOfRace); + + Bearing windBearing = new DegreeBearingImpl(180); + SpeedWithBearing windSpeed = new KnotSpeedWithBearingImpl(15, windBearing); + + Wind wind = new WindImpl(new DegreePosition(54.431952, 10.186767), startOfRace, windSpeed); + WindWithConfidence> windWithConfidence = new WindWithConfidenceImpl<>(wind, 0.5, null, false); + // Always return same wind + when(trackedRace.getWind(any(Position.class), any(TimePoint.class))).thenReturn(wind); + when(trackedRace.getWind(any(Position.class), any(TimePoint.class), any())).thenReturn(wind); + when(trackedRace.getWindWithConfidence(any(Position.class), any(TimePoint.class), any())).thenReturn(windWithConfidence); + + return trackedRace; + } + + private MarkPassing createMockedStartMarkPassing() { + Calendar cal = Calendar.getInstance(); + cal.set(2014, 4, 3, 12, 15); + TimePoint startOfRaceForCompetitor = new MillisecondsTimePoint(cal.getTime()); + + MarkPassing passing = mock(MarkPassing.class); + when(passing.getTimePoint()).thenReturn(startOfRaceForCompetitor); + return passing; + } + + private RaceDefinition createMockedRaceDefinition() { + RaceDefinition raceDefinition = mock(RaceDefinition.class); + Course mockedCourse = createMockedCourse(); + when(raceDefinition.getCourse()).thenReturn(mockedCourse); + BoatClass mockedBoatClass = mock(BoatClass.class); + when(mockedBoatClass.getManeuverDegreeAngleThreshold()).thenReturn(20.0); + when(raceDefinition.getBoatClass()).thenReturn(mockedBoatClass); + return raceDefinition; + } + + private Course createMockedCourse() { + Course course = mock(Course.class); + return course; + } + +} diff --git a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/regression/test/IncrementalLinearRegressionTest.java b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/regression/test/IncrementalLinearRegressionTest.java new file mode 100644 index 00000000000..05823d6064f --- /dev/null +++ b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/regression/test/IncrementalLinearRegressionTest.java @@ -0,0 +1,233 @@ +package com.sap.sailing.polars.regression.test; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import org.hamcrest.number.IsCloseTo; +import org.junit.Test; + +import com.sap.sailing.polars.regression.IncrementalLinearRegressionProcessor; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; +import com.sap.sailing.polars.regression.impl.IncrementalLeastSquaresProcessor; + +public class IncrementalLinearRegressionTest { + + private static double EPSILON = 1E-12; + + private static double VAGUE_EPSILON = 0.1; + + private IncrementalLinearRegressionProcessor createRegressionProcessor() { + IncrementalLinearRegressionProcessor processor = new IncrementalLeastSquaresProcessor(); + return processor; + } + + @Test + public void testRegressionWithTwoDataPoints() throws NotEnoughDataHasBeenAddedException { + IncrementalLinearRegressionProcessor processor = createRegressionProcessor(); + + double x1 = 3; + double y1 = 7; + processor.addMeasuredPoint(x1, y1); + + double x2 = 6; + double y2 = 14; + processor.addMeasuredPoint(x2, y2); + + double y = processor.getEstimatedY(x1); + assertThat(y, new IsCloseTo(y1, EPSILON)); + + y = processor.getEstimatedY(x2); + assertThat(y, new IsCloseTo(y2, EPSILON)); + + y = processor.getEstimatedY(4.5); + assertThat(y, new IsCloseTo(10.5, EPSILON)); + } + + @Test + public void testRegressionWithMultipleDataPoints() throws NotEnoughDataHasBeenAddedException { + IncrementalLinearRegressionProcessor processor = createRegressionProcessor(); + + for (int i = -50; i < 51; i++) { + processor.addMeasuredPoint(i, i); + } + + double y = processor.getEstimatedY(-2); + assertThat(y, new IsCloseTo(-2, EPSILON)); + + // 2 Points off the line + processor.addMeasuredPoint(-8, -3); + processor.addMeasuredPoint(8, -3); + + + y = processor.getEstimatedY(-2); + assertThat(y, new IsCloseTo(-2, VAGUE_EPSILON)); + y = processor.getEstimatedY(20); + assertThat(y, new IsCloseTo(20, VAGUE_EPSILON)); + } + + @Test + public void testRegressionWithABigAmountOfDataPoints() throws NotEnoughDataHasBeenAddedException { + IncrementalLinearRegressionProcessor processor = createRegressionProcessor(); + + for (int i = -1000; i < 1000; i++) { + // For every even number + 1, else - 1 + double y = i % 2 == 0 ? i + 1 : i - 1; + processor.addMeasuredPoint(i, y); + } + + double y = processor.getEstimatedY(-2); + assertThat(y, new IsCloseTo(-2, VAGUE_EPSILON)); + y = processor.getEstimatedY(20); + assertThat(y, new IsCloseTo(20, VAGUE_EPSILON)); + } + + @Test + public void testRegressionWithZeroValueData() throws NotEnoughDataHasBeenAddedException { + IncrementalLinearRegressionProcessor processor = createRegressionProcessor(); + + double x1 = 0; + double y1 = 0; + processor.addMeasuredPoint(x1, y1); + + double x2 = 5; + double y2 = 0; + processor.addMeasuredPoint(x2, y2); + + double y = processor.getEstimatedY(x1); + assertThat(y, new IsCloseTo(y1, EPSILON)); + + y = processor.getEstimatedY(x2); + assertThat(y, new IsCloseTo(y2, EPSILON)); + } + + @Test + public void testRegressionWithNegativeData() throws NotEnoughDataHasBeenAddedException { + IncrementalLinearRegressionProcessor processor = createRegressionProcessor(); + + double x1 = -5; + double y1 = -5; + processor.addMeasuredPoint(x1, y1); + + double x2 = 5; + double y2 = 5; + processor.addMeasuredPoint(x2, y2); + + double y = processor.getEstimatedY(x1); + assertThat(y, new IsCloseTo(y1, EPSILON)); + + y = processor.getEstimatedY(x2); + assertThat(y, new IsCloseTo(y2, EPSILON)); + + y = processor.getEstimatedY(0); + assertThat(y, new IsCloseTo(0, EPSILON)); + } + + @Test + public void testRegressionWithSmallValuedData() throws NotEnoughDataHasBeenAddedException { + IncrementalLinearRegressionProcessor processor = createRegressionProcessor(); + + double x1 = 5E-9; + double y1 = 5E-9; + for (int i = 0; i < 1000; i++) { + double yTimesI = y1 * i; + double xTimesI = x1 * i; + processor.addMeasuredPoint(xTimesI, yTimesI); + if (i > 0) { + double y = processor.getEstimatedY(xTimesI); + assertThat(y, new IsCloseTo(yTimesI, EPSILON)); + } + } + // Add data that is off the line but offsetting each other + processor.addMeasuredPoint(-x1, y1); + processor.addMeasuredPoint(x1, -y1); + + double y = processor.getEstimatedY(0); + assertThat(y, new IsCloseTo(0, EPSILON)); + } + + @Test(expected = NotEnoughDataHasBeenAddedException.class) + public void testNoDataExceptionThrowing() throws NotEnoughDataHasBeenAddedException { + IncrementalLinearRegressionProcessor processor = createRegressionProcessor(); + processor.getEstimatedY(15); + } + + /** + * Assert that y is constant for only one added data point + * + * @throws NotEnoughDataHasBeenAddedException + */ + @Test + public void testRegressionWithOneDataPoint() throws NotEnoughDataHasBeenAddedException { + IncrementalLinearRegressionProcessor processor = createRegressionProcessor(); + + double x1 = 3; + double y1 = 5; + processor.addMeasuredPoint(x1, y1); + + double y = processor.getEstimatedY(20); + assertThat(y, is(y1)); + } + + /* + * The following tests and the data are taken from: org.apache.commons.math3.stat.regression.SimpleRegressionTest + * + * http://svn.apache.org/viewvc/commons/proper/math/trunk/src/test/java/org/apache/commons/math3/stat/regression/ + * SimpleRegressionTest.java?view=markup + * + * They are adapted to use the IncrementalLinearRegressionProcessor, but the data and the asserts were kept as is + */ + + /* + * NIST "Norris" refernce data set from + * http://www.itl.nist.gov/div898/strd/lls/data/LINKS/DATA/Norris.dat + * Strangely, order is {y,x} + */ + private double[][] data = { { 0.1, 0.2 }, {338.8, 337.4 }, {118.1, 118.2 }, + {888.0, 884.6 }, {9.2, 10.1 }, {228.1, 226.5 }, {668.5, 666.3 }, {998.5, 996.3 }, + {449.1, 448.6 }, {778.9, 777.0 }, {559.2, 558.2 }, {0.3, 0.4 }, {0.1, 0.6 }, {778.1, 775.5 }, + {668.8, 666.9 }, {339.3, 338.0 }, {448.9, 447.5 }, {10.8, 11.6 }, {557.7, 556.0 }, + {228.3, 228.1 }, {998.0, 995.8 }, {888.8, 887.6 }, {119.6, 120.2 }, {0.3, 0.3 }, + {0.6, 0.3 }, {557.6, 556.8 }, {339.3, 339.1 }, {888.0, 887.2 }, {998.5, 999.0 }, + {778.9, 779.0 }, {10.2, 11.1 }, {117.6, 118.3 }, {228.9, 229.2 }, {668.4, 669.1 }, + {449.2, 448.9 }, {0.2, 0.5 } + }; + + + @Test + public void testNorris() throws NotEnoughDataHasBeenAddedException { + IncrementalLinearRegressionProcessor regression = createRegressionProcessor(); + for (int i = 0; i < data.length; i++) { + regression.addMeasuredPoint(data[i][1], data[i][0]); + } + // Tests against certified values from + // http://www.itl.nist.gov/div898/strd/lls/data/LINKS/DATA/Norris.dat + assertThat(regression.getSlope(), new IsCloseTo(1.00211681802045, EPSILON)); + assertThat(regression.getIntercept(), new IsCloseTo(-0.262323073774029, EPSILON)); + // ------------ End certified data tests + assertThat(regression.getEstimatedY(0), new IsCloseTo(-0.262323073774029, EPSILON)); + assertThat(regression.getEstimatedY(1), new IsCloseTo(1.00211681802045 - 0.26232307377402, EPSILON)); + } + + @Test + public void testPerfect() { + IncrementalLinearRegressionProcessor regression = createRegressionProcessor(); + int n = 100; + for (int i = 0; i < n; i++) { + regression.addMeasuredPoint(((double) i) / (n - 1), i); + } + assertTrue(regression.getSlope() > 0); + } + + @Test + public void testPerfectNegative() { + IncrementalLinearRegressionProcessor regression = createRegressionProcessor(); + int n = 100; + for (int i = 0; i < n; i++) { + regression.addMeasuredPoint(-((double) i) / (n - 1), i); + } + assertTrue(regression.getSlope() < 0); + } + + +} diff --git a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/test/PolarSheetAnalyzerTest.java b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/test/PolarSheetAnalyzerTest.java new file mode 100644 index 00000000000..bc49367a62a --- /dev/null +++ b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/test/PolarSheetAnalyzerTest.java @@ -0,0 +1,183 @@ +package com.sap.sailing.polars.test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.closeTo; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Test; +import org.mockito.ArgumentMatcher; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; +import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +import com.sap.sailing.polars.PolarDataService; +import com.sap.sailing.polars.analysis.PolarSheetAnalyzer; +import com.sap.sailing.polars.analysis.impl.PolarSheetAnalyzerImpl; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; + +public class PolarSheetAnalyzerTest { + + @Test + public void testSpeedAndBearingCalculation() throws NotEnoughDataHasBeenAddedException { + + PolarSheetAnalyzer analyzer = new PolarSheetAnalyzerImpl(createMockedPolarDataService()); + BoatClass boatClass = mock(BoatClass.class); + + SpeedWithBearingWithConfidence result = analyzer.getAverageUpwindSpeedWithBearingOnStarboardTackFor(boatClass, + new KnotSpeedImpl(14), true); + assertThat(result.getObject().getKnots(), closeTo(8.5, 0.1)); + assertThat(result.getObject().getBearing().getDegrees(), closeTo(50, 0.1)); + assertThat(result.getConfidence(), closeTo(0.7, 0.1)); + + SpeedWithBearingWithConfidence result2 = analyzer.getAverageDownwindSpeedWithBearingOnStarboardTackFor(boatClass, + new KnotSpeedImpl(14), true); + assertThat(result2.getObject().getKnots(), closeTo(14.2, 0.1)); + assertThat(result2.getObject().getBearing().getDegrees(), closeTo(142.8, 0.1)); + assertThat(result2.getConfidence(), closeTo(0.6, 0.1)); + + SpeedWithBearingWithConfidence result3 = analyzer.getAverageUpwindSpeedWithBearingOnPortTackFor(boatClass, + new KnotSpeedImpl(14), true); + assertThat(result3.getObject().getKnots(), closeTo(8.6, 0.1)); + assertThat(result3.getObject().getBearing().getDegrees(), closeTo(-49.5, 0.1)); + assertThat(result3.getConfidence(), closeTo(0.7, 0.1)); + + SpeedWithBearingWithConfidence result4 = analyzer.getAverageDownwindSpeedWithBearingOnPortTackFor(boatClass, + new KnotSpeedImpl(14), true); + assertThat(result4.getObject().getKnots(), closeTo(13.5, 0.1)); + assertThat(result4.getObject().getBearing().getDegrees(), closeTo(-143.6, 0.1)); + assertThat(result4.getConfidence(), closeTo(0.25, 0.1)); + + } + + private PolarDataService createMockedPolarDataService() throws NotEnoughDataHasBeenAddedException { + PolarDataService mockedPolarDataService = mock(PolarDataService.class); + Integer[] data = createCounts(); + when(mockedPolarDataService.getDataCountsForWindSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + any(Integer.class), any(Integer.class))).thenReturn(data); + + //Starboard Upwind + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(49)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(8.478048671702888), 0.5, null)); + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(50)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(8.466303997538812), 0.5, null)); + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(51)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(8.583383077026435), 0.5, null)); + + //Starboard Downwind + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(141)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(13.794354053931528), 0.5, null)); + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(142)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(13.665782579628802), 0.5, null)); + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(143)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(14.169301888320263), 0.5, null)); + + //Port Upwind + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(-48)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(8.445599410456111), 0.5, null)); + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(-50)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(8.553274292235153), 0.5, null)); + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(-49)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(8.614582090896583), 0.5, null)); + + //Port Downwind + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(-145)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(13.78894715705271), 0.5, null)); + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(-144)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(13.420656294986587), 0.5, null)); + when(mockedPolarDataService.getSpeed(any(BoatClass.class), argThat(new SpeedMatcher(14)), + argThat(new BearingMatcher(-143)), true)).thenReturn( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(13.268607457651942), 0.5, null)); + + return mockedPolarDataService; + } + + /** + * The following data is taken from a real race. (49ER yellow R2, KW 2014) + * Only the data for windspeed 14kn is used. The first block contains the + * boatspeeds for every angle to the wind. The second block contains the + * datacount for each angle (number of underlying fixes). + * + * @return + */ + private Integer[] createCounts() { + Integer[] dataCountsPerAngle = { 0, 0, 2, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 3, 1, 2, 1, 6, 2, 2, 2, 1, 6, 6, 17, + 13, 19, 30, 43, 32, 50, 48, 57, 70, 76, 107, 129, 177, 186, 222, 237, 275, 285, 321, 322, 401, 451, + 466, 413, 468, 479, 459, 429, 360, 345, 317, 288, 278, 202, 175, 130, 113, 104, 100, 73, 45, 51, 52, + 37, 40, 36, 26, 23, 32, 33, 29, 29, 22, 24, 27, 23, 22, 23, 22, 21, 15, 18, 20, 14, 15, 21, 20, 19, 6, + 12, 10, 9, 10, 8, 1, 9, 6, 6, 14, 16, 7, 7, 6, 2, 5, 3, 6, 5, 8, 7, 14, 14, 12, 9, 20, 14, 22, 28, 14, + 20, 22, 29, 30, 38, 45, 50, 72, 87, 117, 114, 110, 145, 148, 156, 165, 150, 194, 224, 211, 196, 199, + 198, 182, 155, 125, 85, 69, 65, 49, 46, 61, 47, 48, 51, 45, 21, 14, 8, 9, 8, 8, 3, 7, 5, 2, 10, 8, 7, + 7, 10, 8, 13, 13, 11, 5, 9, 9, 8, 15, 6, 3, 5, 11, 9, 12, 5, 13, 12, 18, 15, 31, 25, 35, 36, 39, 59, + 71, 55, 52, 64, 72, 66, 70, 89, 104, 81, 120, 130, 117, 107, 113, 137, 136, 126, 108, 103, 92, 85, 76, + 65, 75, 75, 70, 57, 49, 49, 35, 44, 35, 18, 22, 17, 18, 10, 6, 4, 10, 9, 10, 8, 4, 4, 4, 10, 4, 4, 4, + 9, 8, 8, 7, 7, 6, 4, 7, 4, 1, 6, 6, 4, 6, 5, 5, 5, 6, 5, 9, 7, 12, 16, 10, 25, 25, 30, 29, 47, 44, 31, + 69, 68, 79, 62, 66, 86, 72, 84, 89, 75, 73, 62, 104, 110, 147, 137, 170, 204, 310, 347, 354, 377, 433, + 457, 419, 436, 467, 502, 513, 525, 463, 422, 405, 363, 309, 282, 262, 186, 162, 123, 97, 81, 45, 35, + 30, 24, 20, 12, 9, 8, 5, 6, 4, 6, 7, 2, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 1 }; + return dataCountsPerAngle; + } + + private class SpeedMatcher extends ArgumentMatcher { + + private final double speedToMatchInKnots; + + public SpeedMatcher(double speedToMatchInKnots) { + this.speedToMatchInKnots = speedToMatchInKnots; + } + + + @Override + public boolean matches(Object argument) { + boolean result = false; + if (argument != null) { + Speed speed = (Speed) argument; + if (speed.getKnots() > speedToMatchInKnots - 0.05 && speed.getKnots() < speedToMatchInKnots + 0.05) { + result = true; + } + } + return result; + } + + } + + private class BearingMatcher extends ArgumentMatcher { + + private final double bearingToMatchInDegrees; + + public BearingMatcher(double bearingToMatchInDegrees) { + this.bearingToMatchInDegrees = bearingToMatchInDegrees; + } + + @Override + public boolean matches(Object argument) { + boolean result = false; + if (argument != null) { + Bearing bearing = (Bearing) argument; + if (bearing.getDegrees() > bearingToMatchInDegrees - 0.4999999 + && bearing.getDegrees() < bearingToMatchInDegrees + 0.49999999) { + result = true; + } + } + return result; + } + + } + +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/PolarSheetGenerationTest.java b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/test/PolarSheetGenerationTest.java similarity index 72% rename from java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/PolarSheetGenerationTest.java rename to java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/test/PolarSheetGenerationTest.java index 59120e0fbc6..ac13fb065fe 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/PolarSheetGenerationTest.java +++ b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/test/PolarSheetGenerationTest.java @@ -1,4 +1,4 @@ -package com.sap.sailing.domain.test; +package com.sap.sailing.polars.test; import java.util.ArrayList; import java.util.HashMap; @@ -9,6 +9,7 @@ import java.util.NavigableSet; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; @@ -42,10 +43,6 @@ import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; import com.sap.sailing.domain.common.impl.PolarSheetGenerationSettingsImpl; import com.sap.sailing.domain.common.impl.WindSourceImpl; import com.sap.sailing.domain.common.impl.WindSteppingWithMaxDistance; -import com.sap.sailing.domain.polarsheets.DataPointWithOriginInfo; -import com.sap.sailing.domain.polarsheets.PerRaceAndCompetitorPolarSheetGenerationWorker; -import com.sap.sailing.domain.polarsheets.PolarSheetGenerationWorker; -import com.sap.sailing.domain.polarsheets.PolarSheetHistogramBuilder; import com.sap.sailing.domain.test.mock.MockedTrackedRace; import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; import com.sap.sailing.domain.tracking.GPSFixMoving; @@ -59,13 +56,21 @@ import com.sap.sailing.domain.tracking.impl.MarkPassingByTimeComparator; import com.sap.sailing.domain.tracking.impl.MarkPassingImpl; import com.sap.sailing.domain.tracking.impl.WindImpl; import com.sap.sailing.domain.tracking.impl.WindWithConfidenceImpl; +import com.sap.sailing.polars.aggregation.PolarFixAggregationWorker; +import com.sap.sailing.polars.aggregation.PolarFixAggregator; +import com.sap.sailing.polars.aggregation.SimplePolarFixRaceInterval; +import com.sap.sailing.polars.data.DataPointWithOriginInfo; +import com.sap.sailing.polars.data.PolarFix; +import com.sap.sailing.polars.data.impl.DataPointWithOriginInfoImpl; +import com.sap.sailing.polars.generation.PolarSheetGenerator; +import com.sap.sailing.polars.generation.PolarSheetHistogramBuilder; import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsTimePoint; public class PolarSheetGenerationTest { @Test - public void testPolarSheetRawDataGeneration() throws InterruptedException { + public void testPolarSheetRawDataGeneration() throws InterruptedException, ExecutionException { Executor executor = new ThreadPoolExecutor(/* corePoolSize */Runtime.getRuntime().availableProcessors(), /* maximumPoolSize */Runtime.getRuntime().availableProcessors(), /* keepAliveTime */60, TimeUnit.SECONDS, @@ -73,7 +78,7 @@ public class PolarSheetGenerationTest { MockTrackedRaceForPolarSheetGeneration race = new MockTrackedRaceForPolarSheetGeneration(); - Integer[] levels = { 4, 6, 8, 10, 12, 14, 16, 20, 25, 30 }; + Double[] levels = { 4., 6., 8., 10., 12., 14., 16., 20., 25., 30. }; WindSteppingWithMaxDistance windStepping = new WindSteppingWithMaxDistance(levels, 2.0); PolarSheetGenerationSettings settings = new PolarSheetGenerationSettingsImpl(1, 0, 1, 20, 0, false, true, 5, 0.05, false, windStepping, false); @@ -81,27 +86,29 @@ public class PolarSheetGenerationTest { TimePoint startTime = new MillisecondsTimePoint(9); TimePoint endTime = new MillisecondsTimePoint(80); // Only used for storing and exporting results in this test case: - PolarSheetGenerationWorker resultContainer = new PolarSheetGenerationWorker(new HashSet(), + PolarFixAggregator resultContainer = new PolarFixAggregator(new SimplePolarFixRaceInterval( + new HashSet()), settings, executor); BoatClass forelle = new BoatClassImpl("Forelle", true); Competitor competitor = new CompetitorImpl(UUID.randomUUID(), "Hans Frantz", Color.RED, new TeamImpl("SAP", null, null), new BoatImpl("Schnelle Forelle", forelle, "GER000")); - PerRaceAndCompetitorPolarSheetGenerationWorker task = new PerRaceAndCompetitorPolarSheetGenerationWorker(race, - resultContainer, startTime, endTime, competitor, settings); + PolarFixAggregationWorker task = new PolarFixAggregationWorker(race, + resultContainer, startTime, endTime, + competitor, settings, null); executor.execute(task); - double timeUntilTimeout = 1000; while (!task.isDone() && timeUntilTimeout > 0) { Thread.sleep(100); timeUntilTimeout = timeUntilTimeout - 0.1; } + + Set fixes = resultContainer.getAggregationResultAsSingleList(); - Assert.assertTrue(task.isDone()); - - PolarSheetsData data = resultContainer.getPolarData(); + PolarSheetGenerator generator = new PolarSheetGenerator(fixes, settings); + PolarSheetsData data = generator.generate(); Assert.assertEquals(4, data.getDataCount()); Assert.assertEquals(4.0, data.getAveragedPolarDataByWindSpeed()[0][45]); Assert.assertEquals(2.0, data.getAveragedPolarDataByWindSpeed()[0][55]); @@ -111,7 +118,7 @@ public class PolarSheetGenerationTest { @Test public void testHistogramBuilder() { - Integer[] levels = { 4, 6, 8, 10, 12, 14, 16, 20, 25, 30 }; + Double[] levels = { 4., 6., 8., 10., 12., 14., 16., 20., 25., 30. }; WindSteppingWithMaxDistance windStepping = new WindSteppingWithMaxDistance(levels, 2.0); PolarSheetGenerationSettings settings = new PolarSheetGenerationSettingsImpl(1, 0, 1, 10, 0, false, true, 5, 0.05, false, windStepping, false); @@ -119,15 +126,15 @@ public class PolarSheetGenerationTest { List rawData = new ArrayList(); - rawData.add(new DataPointWithOriginInfo(1.09, "", "")); - rawData.add(new DataPointWithOriginInfo(1.0, "", "")); - rawData.add(new DataPointWithOriginInfo(1.11, "", "")); - rawData.add(new DataPointWithOriginInfo(1.46, "", "")); - rawData.add(new DataPointWithOriginInfo(1.56, "", "")); - rawData.add(new DataPointWithOriginInfo(2.05, "", "")); - rawData.add(new DataPointWithOriginInfo(2.09, "", "")); - rawData.add(new DataPointWithOriginInfo(3.0, "", "")); - rawData.add(new DataPointWithOriginInfo(2.999, "", "")); + rawData.add(new DataPointWithOriginInfoImpl(1.09, "", "")); + rawData.add(new DataPointWithOriginInfoImpl(1.0, "", "")); + rawData.add(new DataPointWithOriginInfoImpl(1.11, "", "")); + rawData.add(new DataPointWithOriginInfoImpl(1.46, "", "")); + rawData.add(new DataPointWithOriginInfoImpl(1.56, "", "")); + rawData.add(new DataPointWithOriginInfoImpl(2.05, "", "")); + rawData.add(new DataPointWithOriginInfoImpl(2.09, "", "")); + rawData.add(new DataPointWithOriginInfoImpl(3.0, "", "")); + rawData.add(new DataPointWithOriginInfoImpl(2.999, "", "")); PolarSheetsHistogramData result = builder.build(rawData, 0, 0); Number[] xValues = result.getxValues(); Assert.assertEquals(10, xValues.length); @@ -144,49 +151,49 @@ public class PolarSheetGenerationTest { public void testOutlierNeighborhoodAlgorithm() { PolarSheetGenerationSettings settings = PolarSheetGenerationSettingsImpl.createStandardPolarSettings(); List values = new ArrayList(); - values.add(new DataPointWithOriginInfo(0., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); - values.add(new DataPointWithOriginInfo(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(0., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); + values.add(new DataPointWithOriginInfoImpl(20., "", "")); int[] count = new int[1]; count[0] = values.size(); Map> valuesInMap = new HashMap>(); valuesInMap.put(0, values); - double pct = PolarSheetGenerationWorker.getNeighboorhoodSizePercentage(count, valuesInMap, 0, 0, - new DataPointWithOriginInfo(0.0, "", ""), settings); + double pct = PolarSheetGenerator.getNeighboorhoodSizePercentage(count, valuesInMap, 0, 0, + new DataPointWithOriginInfoImpl(0.0, "", ""), settings); Assert.assertTrue(pct < 0.05); - pct = PolarSheetGenerationWorker.getNeighboorhoodSizePercentage(count, valuesInMap, 0, 2, - new DataPointWithOriginInfo(20.0, "", ""), settings); + pct = PolarSheetGenerator.getNeighboorhoodSizePercentage(count, valuesInMap, 0, 2, + new DataPointWithOriginInfoImpl(20.0, "", ""), settings); Assert.assertTrue(pct > 0.05); diff --git a/java/com.sap.sailing.polars/.classpath b/java/com.sap.sailing.polars/.classpath new file mode 100644 index 00000000000..1f733c48034 --- /dev/null +++ b/java/com.sap.sailing.polars/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/java/com.sap.sailing.polars/.project b/java/com.sap.sailing.polars/.project new file mode 100644 index 00000000000..b2e142dedfa --- /dev/null +++ b/java/com.sap.sailing.polars/.project @@ -0,0 +1,28 @@ + + + com.sap.sailing.polars + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.pde.PluginNature + + diff --git a/java/com.sap.sailing.polars/.settings/org.eclipse.jdt.core.prefs b/java/com.sap.sailing.polars/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..a698e59674f --- /dev/null +++ b/java/com.sap.sailing.polars/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/java/com.sap.sailing.polars/META-INF/MANIFEST.MF b/java/com.sap.sailing.polars/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..647b83c8617 --- /dev/null +++ b/java/com.sap.sailing.polars/META-INF/MANIFEST.MF @@ -0,0 +1,23 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Domain +Bundle-SymbolicName: com.sap.sailing.polars +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ClassPath: . +Export-Package: + com.sap.sailing.polars, + com.sap.sailing.polars.analysis, + com.sap.sailing.polars.data, + com.sap.sailing.polars.factory, + com.sap.sailing.polars.regression +Bundle-ActivationPolicy: lazy +Require-Bundle: + com.sap.sailing.domain.common;bundle-version="1.0.0", + com.sap.sailing.domain, + com.sap.sse.datamining, + com.sap.sse.datamining.shared;bundle-version="1.0.0" +Import-Package: + com.sap.sailing.util, + com.sap.sse.common diff --git a/java/com.sap.sailing.polars/build.properties b/java/com.sap.sailing.polars/build.properties new file mode 100644 index 00000000000..34d2e4d2dad --- /dev/null +++ b/java/com.sap.sailing.polars/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/java/com.sap.sailing.polars/pom.xml b/java/com.sap.sailing.polars/pom.xml new file mode 100644 index 00000000000..b42128798f9 --- /dev/null +++ b/java/com.sap.sailing.polars/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + root + com.sap.sailing + 1.0.0-SNAPSHOT + + com.sap.sailing.polars + eclipse-plugin + diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/NoPolarDataAvailableException.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/NoPolarDataAvailableException.java new file mode 100644 index 00000000000..dcefa34b9af --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/NoPolarDataAvailableException.java @@ -0,0 +1,28 @@ +package com.sap.sailing.polars; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Speed; + +public class NoPolarDataAvailableException extends Exception { + + private static final long serialVersionUID = -6939576877174310618L; + + private final BoatClass boatClass; + private final Speed windSpeed; + private final Bearing bearingToTheWind; + + public NoPolarDataAvailableException(BoatClass boatClass, Speed windSpeed, Bearing bearingToTheWind) { + this.boatClass = boatClass; + this.windSpeed = windSpeed; + this.bearingToTheWind = bearingToTheWind; + } + + @Override + public String getMessage() { + return String + .format("There was no polar data available for boat class '%s' for an angle to the wind of %.1f degrees and the wind speed of %.1f knots.", + boatClass.getName(), bearingToTheWind.getDegrees(), windSpeed.getKnots()); + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/PolarDataService.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/PolarDataService.java new file mode 100644 index 00000000000..679e7dc33ed --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/PolarDataService.java @@ -0,0 +1,134 @@ +package com.sap.sailing.polars; + +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.PolarSheetGenerationSettings; +import com.sap.sailing.domain.common.PolarSheetsData; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.polars.analysis.PolarSheetAnalyzer; +import com.sap.sailing.polars.data.PolarFix; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; + +/** + * Public Facade interface allowing access to the polars of {@link BoatClass}es. + * + * It uses a {@link PolarSheetAnalyzer} for more advanced analysis. It's methods are facaded in this interface for + * central access. + * + * The interesting methods for a user are {@link #getSpeed(BoatClass, Speed, Bearing, boolean)} if data for a specific angle is + * needed and {@link #getAverageSpeedWithBearing(BoatClass, Speed, LegType, Tack)} + * which also returns the average angle for the provided parameters. + * + * @author Frederik Petersen (D054528) + * + */ +public interface PolarDataService { + + /** + * + * @param boatClass + * @param windSpeed + * @param bearingToTheWind + * Boat's direction relative to the wind. either in -180 -> +180 or 0 -> 359 degrees + * @param useLinearRegression if true uses lin. Regression for estimation in the wind interval, if false simple arithm. mean in interval + * @return The speed the boat is moving at for the specified wind and bearing according to the polar diagram. + * @throws NotEnoughDataHasBeenAddedException + */ + SpeedWithConfidence getSpeed(BoatClass boatClass, Speed windSpeed, Bearing bearingToTheWind, boolean useLinearRegression) + throws NotEnoughDataHasBeenAddedException; + + /** + * + * @param boatClass + * @param windSpeed + * @param legType + * Should be UpWind or DownWind, there is no information for other courses yet. Use getSpeed for the + * desired angle to get rawer information on other courses for now. + * @param tack + * Polar data can vary depending on the tack the boat is on. + * @return The estimated average speed of a boat for the supplied parameters with the estimated average bearing to + * the wind and a confidence which consists of the confidences of the wind speed, and boat speed sources (50%) + * and a confidence calculated using the amount of underlying fixes (50%). 0 <= confidence < 1
+ * A value with zero confidence doesn't have any significance!

+ * + * The bearing is somewhere between -179 to +180

+ * + * Get the speed using returnValue.getObject()

+ * + * Returns null if the leg type is not up or downwind. + * + * @throws NotEnoughDataHasBeenAddedException + * If there is not enough data to supply a value with some kind of significance. + */ + SpeedWithBearingWithConfidence getAverageSpeedWithBearing(BoatClass boatClass, Speed windSpeed, + LegType legType, Tack tack) throws NotEnoughDataHasBeenAddedException; + + + /** + * Generates a polar sheet for geven races and settings using the provided executor for the worker threads. This + * method does not access a cache for now. + * + * @param trackedRaces + * The set of races to generate the diagram for. + * @param settings + * Settings as supplied by the user. + * @param executor + * The executor to run the worker threads with. + * @return The generated polar sheet with meta data. + */ + PolarSheetsData generatePolarSheet(Set trackedRaces, PolarSheetGenerationSettings settings, + Executor executor) throws InterruptedException, ExecutionException; + + void newRaceFinishedTracking(TrackedRace trackedRace); + + /** + * @param key + * The {@link BoatClass} to obtain fixes for. + * @return All raw polar fixes for the {@link BoatClass}. The implementation is responsible for deciding wether a + * cache is used or not. + */ + Set getPolarFixesForBoatClass(BoatClass key); + + /** + * + * @param boatClass + * The {@link BoatClass} to obtain the polar sheet for. + * @return The polar sheet for all existing races of the {@link BoatClass}. + */ + PolarSheetsData getPolarSheetForBoatClass(BoatClass boatClass); + + /** + * + * @return The {@link BoatClass}es for which there are polar sheets available via + * {@link PolarDataService#getPolarSheetForBoatClass(BoatClass)} + */ + Set getAllBoatClassesWithPolarSheetsAvailable(); + + void competitorPositionChanged(GPSFixMoving fix, Competitor competitor, TrackedRace createdTrackedRace); + + /** + * Returns underlying datacount for a given boat class and windspeed. + * @param boatClass + * @param windSpeed + * @param startAngleInclusive between 0 and 359; smaller than (or equal to) endAngleExclusive + * @param endAngleExclusive between 0 and 359; bigger than startAngleInclusive + * @return array with datacount for all angles in the given area, else null + */ + Integer[] getDataCountsForWindSpeed(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive); + + SpeedWithBearingWithConfidence getAverageSpeedWithBearing(BoatClass boatClass, Speed windSpeed, + LegType legType, Tack tack, boolean useLinReg) throws NotEnoughDataHasBeenAddedException; + + +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/PerRaceAndCompetitorPolarSheetGenerationWorker.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/PolarFixAggregationWorker.java similarity index 70% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/PerRaceAndCompetitorPolarSheetGenerationWorker.java rename to java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/PolarFixAggregationWorker.java index 9b3fefc6072..4c984f8d202 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/PerRaceAndCompetitorPolarSheetGenerationWorker.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/PolarFixAggregationWorker.java @@ -1,4 +1,4 @@ -package com.sap.sailing.domain.polarsheets; +package com.sap.sailing.polars.aggregation; import java.util.ArrayList; import java.util.Collection; @@ -18,23 +18,23 @@ import com.sap.sailing.domain.tracking.GPSFixTrack; import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.WindWithConfidence; +import com.sap.sailing.polars.data.PolarFix; +import com.sap.sailing.polars.data.impl.PolarFixImpl; import com.sap.sse.common.TimePoint; -import com.sap.sse.common.Util; +import com.sap.sse.common.Util.Pair; /** - * Iterates through the fixes of one competitor in one tracked race and fills the {@link PolarSheetGenerationWorker} + * Iterates through the fixes of one competitor in one tracked race and fills the {@link PolarFixAggregator} * with datapoints that are found for the speed of the boat and its angle to the wind * * @author D054528 Frederik Petersen * */ -public class PerRaceAndCompetitorPolarSheetGenerationWorker implements Runnable { - - // private static final Logger logger = Logger.getLogger(PerRaceAndCompetitorPolarSheetGenerationWorker.class.getName()); +public class PolarFixAggregationWorker implements Runnable { private final TrackedRace race; - private final PolarSheetGenerationWorker polarSheetGenerationWorker; + private final PolarFixAggregator polarSheetGenerationWorker; private TimePoint startTime; @@ -50,9 +50,11 @@ public class PerRaceAndCompetitorPolarSheetGenerationWorker implements Runnable private PolarSheetGenerationSettings settings; - public PerRaceAndCompetitorPolarSheetGenerationWorker(TrackedRace race, - PolarSheetGenerationWorker polarSheetGenerationWorker, TimePoint startTime, TimePoint endTime, - Competitor competitor, PolarSheetGenerationSettings settings) { + private Pair intervalBeginningAndEnd; + + public PolarFixAggregationWorker(TrackedRace race, PolarFixAggregator polarSheetGenerationWorker, + TimePoint startTime, TimePoint endTime, Competitor competitor, PolarSheetGenerationSettings settings, + Pair intervalBeginningAndEnd) { super(); this.race = race; this.polarSheetGenerationWorker = polarSheetGenerationWorker; @@ -60,9 +62,24 @@ public class PerRaceAndCompetitorPolarSheetGenerationWorker implements Runnable this.endTime = endTime; this.competitor = competitor; this.settings = settings; + this.intervalBeginningAndEnd = intervalBeginningAndEnd; optimizeStartTime(); optimizeEndTime(); checkIfRaceAborted(); + checkIfIntervalBeforeOrAfterRace(); + } + + private void checkIfIntervalBeforeOrAfterRace() { + if (intervalBeginningAndEnd != null) { + TimePoint intervalStart = intervalBeginningAndEnd.getA(); + if (intervalStart.after(endTime)) { + noConfidence = true; + } + TimePoint intervalEnd = intervalBeginningAndEnd.getB(); + if (intervalEnd.before(startTime)) { + noConfidence = true; + } + } } private void checkIfRaceAborted() { @@ -108,7 +125,17 @@ public class PerRaceAndCompetitorPolarSheetGenerationWorker implements Runnable GPSFixTrack track = race.getTrack(competitor); track.lockForRead(); try { - Iterator fixesIterator = track.getFixesIterator(startTime, true); + Iterator fixesIterator; + if (intervalBeginningAndEnd != null) { + TimePoint startOfIntervalToCheck = intervalBeginningAndEnd.getA().before(startTime) ? startTime + : intervalBeginningAndEnd.getA(); + TimePoint endOfIntervalToCheck = intervalBeginningAndEnd.getB().after(endTime) ? endTime + : intervalBeginningAndEnd.getB(); + fixesIterator = track.getFixes(startOfIntervalToCheck, true, endOfIntervalToCheck, true).iterator(); + } else { + // No timepoint interval given + fixesIterator = track.getFixesIterator(startTime, true); + } TimePoint lastConsideredTimePoint = null; if (finishedEarlyAtWaypoint != -1) { NavigableSet markPassings = race.getMarkPassings(competitor); @@ -136,40 +163,39 @@ public class PerRaceAndCompetitorPolarSheetGenerationWorker implements Runnable private void addFixIfValid(GPSFixTrack track, GPSFixMoving fix) { if (!track.hasDirectionChange(fix.getTimePoint(), race.getRace().getBoatClass() .getManeuverDegreeAngleThreshold())) { - List>>> windWithConfidenceList = new ArrayList>>>(); + List>>> windWithConfidenceList = new ArrayList>>>(); if (settings.useOnlyWindGaugesForWindSpeed()) { if (settings.splitByWindgauges()) { windWithConfidenceList.addAll(addAllWindsOfWindGaugesSplitOneByOne(fix)); } else { windWithConfidenceList - .add(new Util.Pair>>( +.add(new Pair>>( createWindGaugesString(race), race.getWindWithConfidence( fix.getPosition(), fix.getTimePoint(), collectWindSourcesToIgnoreForSpeed()))); } } else { - windWithConfidenceList.add(new Util.Pair>>( + windWithConfidenceList.add(new Pair>>( "Combined", race.getWindWithConfidence(fix.getPosition(), fix.getTimePoint()))); } - for (Util.Pair>> windWithSourceIdStringPair : windWithConfidenceList) { - WindWithConfidence> windWithConfidence = windWithSourceIdStringPair + for (Pair>> windWithSourceIdStringPair : windWithConfidenceList) { + WindWithConfidence> windWithConfidence = windWithSourceIdStringPair .getB(); if (windWithConfidence != null && windWithConfidence.useSpeed() && windWithConfidence.getConfidence() >= settings.getMinimumWindConfidence()) { - PolarFix polarFix = new PolarFix(fix, race, track, windWithConfidence.getObject(), + PolarFix polarFix = new PolarFixImpl(fix, race, track, windWithConfidence.getObject(), settings, windWithSourceIdStringPair.getA()); - polarSheetGenerationWorker.addPolarData(polarFix); + polarSheetGenerationWorker.addPolarFix(race.getRaceIdentifier(), polarFix); } } } } - private Collection>>> addAllWindsOfWindGaugesSplitOneByOne( + private Collection>>> addAllWindsOfWindGaugesSplitOneByOne( GPSFixMoving fix) { Iterable windGaugeSources = race.getWindSources(WindSourceType.EXPEDITION); - List>>> windWithConfidenceList - = new ArrayList>>>(); + List>>> windWithConfidenceList = new ArrayList>>>(); for (WindSource windGaugeSource : windGaugeSources) { Iterable allSources = race.getWindSources(); Set allSourcesButTheSingleWindGaugeSource = new HashSet(); @@ -178,7 +204,7 @@ public class PerRaceAndCompetitorPolarSheetGenerationWorker implements Runnable allSourcesButTheSingleWindGaugeSource.add(windSource); } } - windWithConfidenceList.add(new Util.Pair>>( + windWithConfidenceList.add(new Pair>>( windGaugeSource.getId().toString(), race.getWindWithConfidence(fix.getPosition(), fix.getTimePoint(), allSourcesButTheSingleWindGaugeSource))); } diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/PolarFixAggregator.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/PolarFixAggregator.java new file mode 100644 index 00000000000..543a350f0d6 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/PolarFixAggregator.java @@ -0,0 +1,171 @@ +package com.sap.sailing.polars.aggregation; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.RaceDefinition; +import com.sap.sailing.domain.common.PolarSheetGenerationSettings; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.polars.data.PolarFix; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; + +/** + * Allows extracting data about average speed for each angle creating the foundation of polar-sheet generation. Tasks + * are assigned to the executor per race & per competitor and the results are filled on the go to allow progress + * indication in the polar-sheet itself. + * + * @author D054528 Frederik Petersen + * + */ +public class PolarFixAggregator implements Future>> { + + private final Set workers; + + private final Executor executor; + + private final Map> fixes = new HashMap>(); + + /** + * Will prepare the {@link PolarFixAggregationWorker}s per race & per competitor. This includes determining start + * and end time. + * + * @param updateInterval + * from which the data is to be collected + * @param settings + * @param executor + * executes the tasks upon {@link #startPolarFixAggregation()} + */ + public PolarFixAggregator(PolarFixRaceInterval updateInterval, PolarSheetGenerationSettings settings, + Executor executor) { + this.executor = executor; + workers = new HashSet(); + for (Entry>> entry : updateInterval + .getCompetitorAndTimepointsForRace().entrySet()) { + TrackedRace race = entry.getKey(); + TimePoint startTime = race.getStartOfRace(); + TimePoint endTime = race.getEndOfRace(); + if (endTime == null) { + endTime = race.getTimePointOfNewestEvent(); + } + RaceDefinition raceDefinition = race.getRace(); + + Map> detailedInterval = entry.getValue(); + if (detailedInterval == null) { + Iterable competitors = raceDefinition.getCompetitors(); + for (Competitor competitor : competitors) { + PolarFixAggregationWorker task = new PolarFixAggregationWorker(race, this, startTime, endTime, + competitor, settings, null); + workers.add(task); + } + } else { + for (Entry> competitorEntry : detailedInterval.entrySet()) { + Competitor competitor = competitorEntry.getKey(); + Pair intervalBeginningAndEnd = competitorEntry.getValue(); + PolarFixAggregationWorker task = new PolarFixAggregationWorker(race, this, startTime, endTime, + competitor, settings, intervalBeginningAndEnd); + workers.add(task); + } + } + + } + } + + /** + * Starts the {@link PolarFixAggregationWorker}s + */ + public void startPolarFixAggregation() { + for (Runnable task : workers) { + executor.execute(task); + } + } + + /** + * To be called from {@link PolarFixAggregationWorker} for adding a datapoint to the list of + * results + * wind's speed in knots + */ + protected void addPolarFix(RegattaAndRaceIdentifier raceId, PolarFix polarFix) { + synchronized (fixes) { + List existingList = fixes.get(raceId); + if (existingList == null) { + existingList = new ArrayList(); + fixes.put(raceId, existingList); + } + existingList.add(polarFix); + } + } + + private boolean allWorkersDone() { + boolean complete = true; + for (PolarFixAggregationWorker task : workers) { + if (!task.isDone()) { + complete = false; + break; + } + } + return complete; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return allWorkersDone(); + } + + @Override + public Map> get() throws InterruptedException, ExecutionException { + while (!isDone()) { + Thread.sleep(100); + } + return fixes; + } + + @Override + public Map> get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, + TimeoutException { + long timeRun = 0; + long timeoutInMillis = unit.toMillis(timeout); + while (!isDone() && timeRun < timeoutInMillis) { + Thread.sleep(100); + timeRun = timeRun + 100; + } + if (timeRun >= timeoutInMillis) { + throw new TimeoutException(); + } + return fixes; + } + + public Set getAggregationResultAsSingleList() throws InterruptedException, + ExecutionException { + Set fixes = new HashSet(); + Map> aggregationResult = get(); + for (List list : aggregationResult.values()) { + fixes.addAll(list); + } + return fixes; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/PolarFixRaceInterval.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/PolarFixRaceInterval.java new file mode 100644 index 00000000000..8c8c1a9ffc9 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/PolarFixRaceInterval.java @@ -0,0 +1,14 @@ +package com.sap.sailing.polars.aggregation; + +import java.util.Map; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; + +public interface PolarFixRaceInterval { + + Map>> getCompetitorAndTimepointsForRace(); + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/SimplePolarFixRaceInterval.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/SimplePolarFixRaceInterval.java new file mode 100644 index 00000000000..7912fc3504e --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/aggregation/SimplePolarFixRaceInterval.java @@ -0,0 +1,29 @@ +package com.sap.sailing.polars.aggregation; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; + +public class SimplePolarFixRaceInterval implements PolarFixRaceInterval { + + + private final Map>> intervalMap; + + public SimplePolarFixRaceInterval(Set races) { + intervalMap = new HashMap>>(); + for (TrackedRace race : races) { + intervalMap.put(race, null); + } + } + + @Override + public Map>> getCompetitorAndTimepointsForRace() { + return intervalMap; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/analysis/PolarSheetAnalyzer.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/analysis/PolarSheetAnalyzer.java new file mode 100644 index 00000000000..b0e82141562 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/analysis/PolarSheetAnalyzer.java @@ -0,0 +1,22 @@ +package com.sap.sailing.polars.analysis; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; + +public interface PolarSheetAnalyzer { + + SpeedWithBearingWithConfidence getAverageUpwindSpeedWithBearingOnStarboardTackFor(BoatClass boatClass, + Speed windSpeed, boolean useLinReg) throws NotEnoughDataHasBeenAddedException; + + SpeedWithBearingWithConfidence getAverageDownwindSpeedWithBearingOnStarboardTackFor(BoatClass boatClass, + Speed windSpeed, boolean useLinReg) throws NotEnoughDataHasBeenAddedException; + + SpeedWithBearingWithConfidence getAverageUpwindSpeedWithBearingOnPortTackFor(BoatClass boatClass, + Speed windSpeed, boolean useLinReg) throws NotEnoughDataHasBeenAddedException; + + SpeedWithBearingWithConfidence getAverageDownwindSpeedWithBearingOnPortTackFor(BoatClass boatClass, + Speed windSpeed, boolean useLinReg) throws NotEnoughDataHasBeenAddedException; + +} \ No newline at end of file diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/analysis/impl/PolarSheetAnalyzerImpl.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/analysis/impl/PolarSheetAnalyzerImpl.java new file mode 100644 index 00000000000..4d26f83e94d --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/analysis/impl/PolarSheetAnalyzerImpl.java @@ -0,0 +1,162 @@ +package com.sap.sailing.polars.analysis.impl; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.domain.base.impl.SpeedWithBearingWithConfidenceImpl; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.polars.PolarDataService; +import com.sap.sailing.polars.analysis.PolarSheetAnalyzer; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; + +/** + * Extracts typical measures from polar sheets. + * + * @author Frederik Petersen (D054528) + * + */ +public class PolarSheetAnalyzerImpl implements PolarSheetAnalyzer { + + private static final int MINIMUM_DATA_COUNT_FOR_ONE_ANGLE = 5; + private PolarDataService polarDataService; + + public PolarSheetAnalyzerImpl(PolarDataService polarDataService) { + this.polarDataService = polarDataService; + } + + @Override + public SpeedWithBearingWithConfidence getAverageUpwindSpeedWithBearingOnStarboardTackFor(BoatClass boatClass, Speed windSpeed, boolean useLinReg) + throws NotEnoughDataHasBeenAddedException { + int startAngleInclusive = 1; + int endAngleExclusive = 90; + SpeedWithBearingWithConfidence speedWithBearing = estimateAnglePeakAndAverageSpeed(boatClass, windSpeed, startAngleInclusive, + endAngleExclusive, useLinReg); + return speedWithBearing; + } + + + @Override + public SpeedWithBearingWithConfidence getAverageDownwindSpeedWithBearingOnStarboardTackFor(BoatClass boatClass, Speed windSpeed, boolean useLinReg) + throws NotEnoughDataHasBeenAddedException { + int startAngleInclusive = 91; + int endAngleExclusive = 180; + SpeedWithBearingWithConfidence speedWithBearing = estimateAnglePeakAndAverageSpeed(boatClass, windSpeed, startAngleInclusive, + endAngleExclusive, useLinReg); + return speedWithBearing; + } + + @Override + public SpeedWithBearingWithConfidence getAverageDownwindSpeedWithBearingOnPortTackFor(BoatClass boatClass, Speed windSpeed, boolean useLinReg) + throws NotEnoughDataHasBeenAddedException { + int startAngleInclusive = 181; + int endAngleExclusive = 270; + SpeedWithBearingWithConfidence speedWithBearing = estimateAnglePeakAndAverageSpeed(boatClass, windSpeed, startAngleInclusive, + endAngleExclusive, useLinReg); + return speedWithBearing; + } + + @Override + public SpeedWithBearingWithConfidence getAverageUpwindSpeedWithBearingOnPortTackFor(BoatClass boatClass, Speed windSpeed, boolean useLinReg) + throws NotEnoughDataHasBeenAddedException { + int startAngleInclusive = 271; + int endAngleExclusive = 360; + SpeedWithBearingWithConfidence speedWithBearing = estimateAnglePeakAndAverageSpeed(boatClass, windSpeed, startAngleInclusive, + endAngleExclusive, useLinReg); + return speedWithBearing; + } + + private SpeedWithBearingWithConfidence estimateAnglePeakAndAverageSpeed(BoatClass boatClass, Speed windSpeed, + int startAngleInclusive, int endAngleExclusive, boolean useLinearRegression) throws NotEnoughDataHasBeenAddedException { + Integer[] dataCountPerAngle = getDataCountArray(boatClass, windSpeed, startAngleInclusive, endAngleExclusive); + + + + double estimatedPeak = estimatePeak(boatClass, windSpeed, startAngleInclusive, endAngleExclusive, dataCountPerAngle); + + double convertedAngleIfOver180 = convertAngleIfNecessary(estimatedPeak); + + SpeedWithConfidence averagedSpeedWithConfidence = estimateSpeed(boatClass, windSpeed, + convertedAngleIfOver180, useLinearRegression); + + double originConfidence = averagedSpeedWithConfidence.getConfidence(); + + double overallConfidence = (originConfidence + calcDataCountConfidence(dataCountPerAngle, estimatedPeak)) / 2; + + Bearing bearing = new DegreeBearingImpl(convertedAngleIfOver180); + SpeedWithBearing speedWithBearing = new KnotSpeedWithBearingImpl(averagedSpeedWithConfidence.getObject().getKnots(), bearing); + return new SpeedWithBearingWithConfidenceImpl<>(speedWithBearing, overallConfidence, null); + } + + /** + * Uses the formula 1-​e^​((‑x)/​150) to create a confidence for the estimated peak. + * + * If the peak has 100 data points underlying, the confidence will be approx. 0.5 + * + * @param dataCountPerAngle + * @param estimatedPeak + * @return + */ + private double calcDataCountConfidence(Integer[] dataCountPerAngle, double estimatedPeak) { + int dataCount = dataCountPerAngle[(int) Math.round(estimatedPeak)]; + double confidence = 1 - Math.pow(Math.E, - dataCount / 150); + return confidence; + } + + private Integer[] getDataCountArray(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive) { + Integer[] dataCountPerAngle = polarDataService.getDataCountsForWindSpeed(boatClass, windSpeed, startAngleInclusive, endAngleExclusive); + return dataCountPerAngle; + } + + private double convertAngleIfNecessary(double estimatedPeak) { + double convertedAngleIfOver180 = estimatedPeak; + if (estimatedPeak > 180) { + convertedAngleIfOver180 = estimatedPeak - 360; + } + return convertedAngleIfOver180; + } + + private SpeedWithConfidence estimateSpeed(BoatClass boatClass, Speed windSpeed, double estimatedPeak, + boolean useLinearRegression) throws NotEnoughDataHasBeenAddedException { + SpeedWithConfidence speed = polarDataService.getSpeed(boatClass, windSpeed, new DegreeBearingImpl( + estimatedPeak), useLinearRegression); + return speed; + } + + private double estimatePeak(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive, Integer[] dataCountPerAngle) + throws NotEnoughDataHasBeenAddedException { + + // Find peak by averaging the angles that have at least 50% of the max datacount + List dataCountList = Arrays.asList(Arrays.copyOfRange(dataCountPerAngle, startAngleInclusive, + endAngleExclusive)); + int maxDataCount = Collections.max(dataCountList); + if (maxDataCount < MINIMUM_DATA_COUNT_FOR_ONE_ANGLE) { + // The angle with the most data points doesn't have sufficient data, for the polar data to have any + // significance. + throw new NotEnoughDataHasBeenAddedException("Only " + maxDataCount + + " points have been added for the angle with the most points. No significance."); + } + int sumOfAllUpperHalfAngles = 0; + int numberOfAnglesAdded = 0; + for (int i = startAngleInclusive; i < endAngleExclusive; i++) { + Integer dataCount = dataCountPerAngle[i]; + if (dataCount > maxDataCount / 2) { + sumOfAllUpperHalfAngles = sumOfAllUpperHalfAngles + i * dataCount; + numberOfAnglesAdded = numberOfAnglesAdded + dataCount; + } + } + + double estimatedPeak = (double) sumOfAllUpperHalfAngles / (double) numberOfAnglesAdded; + return estimatedPeak; + } + + + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/NoCacheEntryException.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/NoCacheEntryException.java new file mode 100644 index 00000000000..b4cfec4d06a --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/NoCacheEntryException.java @@ -0,0 +1,27 @@ +package com.sap.sailing.polars.caching; + +import java.util.Set; + +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.polars.data.PolarFix; + +public class NoCacheEntryException extends Exception { + + private static final long serialVersionUID = 5547683568438235668L; + private final Set notCached; + private final Set resultList; + + public NoCacheEntryException(Set notCached, Set resultList) { + this.notCached = notCached; + this.resultList = resultList; + } + + public Set getNotCached() { + return notCached; + } + + public Set getCachedResultList() { + return resultList; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCache.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCache.java new file mode 100644 index 00000000000..c9306b315f2 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCache.java @@ -0,0 +1,67 @@ +package com.sap.sailing.polars.caching; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Executor; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.polars.data.PolarFix; +import com.sap.sailing.util.SmartFutureCache; + +public class PolarFixCache extends + SmartFutureCache>, PolarFixCacheRaceInterval> { + + private final List cacheUpdateDoneListeners = new ArrayList(); + + public PolarFixCache(Executor executor) { + super(new PolarFixCacheUpdater(executor), "polarFixCache"); + } + + @Override + protected void cache(BoatClass key, Map> value) { + super.cache(key, value); + synchronized (cacheUpdateDoneListeners) { + for (PolarFixCacheUpdateDoneListener listener : cacheUpdateDoneListeners) { + listener.cacheUpdateDoneForBoatClass(key); + } + } + } + + public void addListener(PolarFixCacheUpdateDoneListener listener) { + synchronized (cacheUpdateDoneListeners) { + cacheUpdateDoneListeners.add(listener); + } + } + + public void removeListener(PolarFixCacheUpdateDoneListener listener) { + synchronized (cacheUpdateDoneListeners) { + if (cacheUpdateDoneListeners.contains(listener)) { + cacheUpdateDoneListeners.remove(listener); + } + } + } + + public Set getFixesForTrackedRaces(Set trackedRaces) throws NoCacheEntryException { + Set resultList = new HashSet(); + Set notCached = new HashSet(); + for (TrackedRace trackedRace : trackedRaces) { + Map> result = get(trackedRace.getRace().getBoatClass(), false); + List resultForRace = result.get(trackedRace.getRaceIdentifier()); + if (resultForRace == null) { + notCached.add(trackedRace); + } else { + resultList.addAll(resultForRace); + } + } + if (notCached.size() > 0) { + throw new NoCacheEntryException(notCached, resultList); + } + return resultList; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCacheRaceInterval.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCacheRaceInterval.java new file mode 100644 index 00000000000..ed6717bc50f --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCacheRaceInterval.java @@ -0,0 +1,97 @@ +package com.sap.sailing.polars.caching; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.polars.aggregation.PolarFixRaceInterval; +import com.sap.sailing.util.SmartFutureCache.UpdateInterval; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; + +public class PolarFixCacheRaceInterval implements UpdateInterval, PolarFixRaceInterval { + + private final Map>> competitorAndTimepointsForRace; + + public PolarFixCacheRaceInterval( + Map>> competitorAndTimepointsForRace) { + this.competitorAndTimepointsForRace = competitorAndTimepointsForRace; + } + + @Override + public Map>> getCompetitorAndTimepointsForRace() { + return competitorAndTimepointsForRace; + } + + @Override + public PolarFixCacheRaceInterval join(PolarFixCacheRaceInterval otherUpdateInterval) { + Map>> joinedInterval = new HashMap>>(); + Set racesContainedInEitherMap = new HashSet(); + for (TrackedRace race : competitorAndTimepointsForRace.keySet()) { + racesContainedInEitherMap.add(race); + } + Map>> otherUpdateIntervalInExplicitForm = otherUpdateInterval + .getCompetitorAndTimepointsForRace(); + for (TrackedRace race : otherUpdateIntervalInExplicitForm.keySet()) { + racesContainedInEitherMap.add(race); + } + + for (TrackedRace race : racesContainedInEitherMap) { + HashMap> joinedMap = new HashMap>(); + Map> mapOfTimpointCompetitorPairs = competitorAndTimepointsForRace + .get(race); + if (mapOfTimpointCompetitorPairs != null) { + joinedMap.putAll(mapOfTimpointCompetitorPairs); + } + Map> mapOfOtherTimpointCompetitorPairs = otherUpdateIntervalInExplicitForm + .get(race); + if (mapOfOtherTimpointCompetitorPairs != null) { + mergeMap(joinedMap, mapOfOtherTimpointCompetitorPairs); + } + joinedInterval.put(race, joinedMap); + } + return new PolarFixCacheRaceInterval(joinedInterval); + } + + private void mergeMap(HashMap> joinedMap, + Map> mapOfOtherTimpointCompetitorPairs) { + for (Entry> otherEntry : mapOfOtherTimpointCompetitorPairs.entrySet()) { + + Competitor competitor = otherEntry.getKey(); + Pair otherValue = otherEntry.getValue(); + Pair existingValue = joinedMap.get(competitor); + if (existingValue != null) { + Pair joinedTimepointInterval = joinTimepointIntervals(existingValue, otherValue); + joinedMap.put(competitor, joinedTimepointInterval); + } else { + joinedMap.put(competitor, otherValue); + } + + } + } + + /** + * This method ignores a possible gap between the two intervals. + * + * @param existingValue + * @param otherValue + * @return + */ + private Pair joinTimepointIntervals(Pair existingValue, + Pair otherValue) { + TimePoint startExisting = existingValue.getA(); + TimePoint endExisting = existingValue.getB(); + TimePoint startOther = otherValue.getA(); + TimePoint endOther = otherValue.getB(); + + TimePoint startNew = startExisting.before(startOther) ? startExisting : startOther; + TimePoint endNew = endExisting.after(endOther) ? endExisting : endOther; + + return new Pair(startNew, endNew); + } + +} \ No newline at end of file diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCacheUpdateDoneListener.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCacheUpdateDoneListener.java new file mode 100644 index 00000000000..75a32432e04 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCacheUpdateDoneListener.java @@ -0,0 +1,9 @@ +package com.sap.sailing.polars.caching; + +import com.sap.sailing.domain.base.BoatClass; + +public interface PolarFixCacheUpdateDoneListener { + + void cacheUpdateDoneForBoatClass(BoatClass key); + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCacheUpdater.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCacheUpdater.java new file mode 100644 index 00000000000..db782760f85 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarFixCacheUpdater.java @@ -0,0 +1,56 @@ +package com.sap.sailing.polars.caching; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.Executor; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.impl.PolarSheetGenerationSettingsImpl; +import com.sap.sailing.polars.aggregation.PolarFixAggregator; +import com.sap.sailing.polars.data.PolarFix; +import com.sap.sailing.util.SmartFutureCache.CacheUpdater; + +public class PolarFixCacheUpdater implements + CacheUpdater>, PolarFixCacheRaceInterval> { + + private final Executor executor; + + public PolarFixCacheUpdater(Executor executor) { + this.executor = executor; + } + + @Override + public Map> computeCacheUpdate(BoatClass key, + PolarFixCacheRaceInterval updateInterval) throws Exception { + PolarFixAggregator aggregator = new PolarFixAggregator(updateInterval, + PolarSheetGenerationSettingsImpl.createStandardPolarSettings(), executor); + Thread.sleep(updateInterval.getCompetitorAndTimepointsForRace().keySet().iterator().next() + .getMillisecondsOverWhichToAverageSpeed() / 2); + aggregator.startPolarFixAggregation(); + + return aggregator.get(); + } + + @Override + public Map> provideNewCacheValue(BoatClass key, + Map> oldCacheValue, + Map> computedCacheUpdate, PolarFixCacheRaceInterval updateInterval) { + Map> newCacheValue = new HashMap>(); + if (oldCacheValue != null) { + newCacheValue.putAll(oldCacheValue); + } + for (Entry> newEntry : computedCacheUpdate.entrySet()) { + List oldList = newCacheValue.get(newEntry.getKey()); + if (oldList != null) { + oldList.addAll(newEntry.getValue()); + } else { + newCacheValue.put(newEntry.getKey(), newEntry.getValue()); + } + } + return newCacheValue; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarSheetPerBoatClassCache.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarSheetPerBoatClassCache.java new file mode 100644 index 00000000000..94e4fa0a4c8 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarSheetPerBoatClassCache.java @@ -0,0 +1,21 @@ +package com.sap.sailing.polars.caching; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.common.PolarSheetsData; +import com.sap.sailing.polars.PolarDataService; +import com.sap.sailing.util.SmartFutureCache; + +public class PolarSheetPerBoatClassCache extends + SmartFutureCache + implements PolarFixCacheUpdateDoneListener { + + public PolarSheetPerBoatClassCache(PolarDataService polarDataService) { + super(new PolarSheetPerBoatClassCacheUpdater(polarDataService), "PolarSheetPerBoatClassCache"); + } + + @Override + public void cacheUpdateDoneForBoatClass(BoatClass key) { + triggerUpdate(key, new EmptyUpdateInterval()); + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarSheetPerBoatClassCacheUpdater.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarSheetPerBoatClassCacheUpdater.java new file mode 100644 index 00000000000..fb94b05ea47 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/caching/PolarSheetPerBoatClassCacheUpdater.java @@ -0,0 +1,36 @@ +package com.sap.sailing.polars.caching; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.common.PolarSheetGenerationSettings; +import com.sap.sailing.domain.common.PolarSheetsData; +import com.sap.sailing.domain.common.impl.PolarSheetGenerationSettingsImpl; +import com.sap.sailing.polars.PolarDataService; +import com.sap.sailing.polars.generation.PolarSheetGenerator; +import com.sap.sailing.util.SmartFutureCache.CacheUpdater; +import com.sap.sailing.util.SmartFutureCache.EmptyUpdateInterval; + +public class PolarSheetPerBoatClassCacheUpdater implements + CacheUpdater { + + private final PolarDataService polarDataService; + + public PolarSheetPerBoatClassCacheUpdater(PolarDataService polarDataService) { + this.polarDataService = polarDataService; + } + + @Override + public PolarSheetsData computeCacheUpdate(BoatClass key, EmptyUpdateInterval updateInterval) throws Exception { + PolarSheetGenerationSettings settings = PolarSheetGenerationSettingsImpl.createStandardPolarSettings(); + PolarSheetGenerator generator = new PolarSheetGenerator(polarDataService.getPolarFixesForBoatClass(key), + settings); + PolarSheetsData generationResult = generator.generate(); + return generationResult; + } + + @Override + public PolarSheetsData provideNewCacheValue(BoatClass key, PolarSheetsData oldCacheValue, + PolarSheetsData computedCacheUpdate, EmptyUpdateInterval updateInterval) { + return computedCacheUpdate; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/clusters/SpeedClusterGroup.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/clusters/SpeedClusterGroup.java new file mode 100644 index 00000000000..a55cf3a3772 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/clusters/SpeedClusterGroup.java @@ -0,0 +1,83 @@ +package com.sap.sailing.polars.clusters; + +import java.util.ArrayList; +import java.util.Collection; + +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +import com.sap.sse.datamining.data.Cluster; +import com.sap.sse.datamining.data.ClusterBoundary; +import com.sap.sse.datamining.impl.data.ClusterWithLowerAndUpperBoundaries; +import com.sap.sse.datamining.impl.data.ComparableClusterBoundary; +import com.sap.sse.datamining.impl.data.ComparisonStrategy; +import com.sap.sse.datamining.impl.data.FixClusterGroup; + +public class SpeedClusterGroup extends FixClusterGroup { + + /** + * A {@link SpeedClusterGroup} lets the user set the level mids for all its clusters. The Boundaries will + * automatically be determined. They are in the middle between each level mid but only if the distance between level + * mid and boundary is smaller or equal {@code maxDistance} + * + * + * @param name + * @param levelMidsInKnots + * sorted low -> high. E.g. [2,4,6,10,15,20,30] + * @param maxDistanceInKnots + * the clusters will max span <-maxDistanceInKnots-|mid|-maxDistanceinKnots-> + */ + public SpeedClusterGroup(String name, double[] levelMidsInKnots, double maxDistanceInKnots) { + super(name, createClustersForLevelMids(levelMidsInKnots, maxDistanceInKnots)); + } + + private static Collection> createClustersForLevelMids(double[] levelMidsInKnots, + double maxDistanceInKnots) { + ArrayList> clusterList = new ArrayList>(); + for (int index = 0; index < levelMidsInKnots.length; index++) { + ClusterBoundary lowerBoundary = createLowerBoundary(levelMidsInKnots, maxDistanceInKnots, index); + ClusterBoundary upperBoundary = createUpperBoundary(levelMidsInKnots, maxDistanceInKnots, index); + Cluster cluster = new ClusterWithLowerAndUpperBoundaries(levelMidsInKnots[index] + "kn", lowerBoundary, + upperBoundary); + clusterList.add(cluster); + } + return clusterList; + } + + private static ClusterBoundary createUpperBoundary(double[] levelMidsInKnots, double maxDistanceInKnots, + int index) { + ClusterBoundary upperBoundary; + double upperBoundaryValue; + if (index == levelMidsInKnots.length - 1) { + upperBoundaryValue = levelMidsInKnots[index] + maxDistanceInKnots; + } else { + double biggestPossibleUpperBoundary = levelMidsInKnots[index] + maxDistanceInKnots; + double midBetweenCurrentLevelMidAndUpperLevelMid = levelMidsInKnots[index] + + (0.5 * (levelMidsInKnots[index + 1] - levelMidsInKnots[index])); + upperBoundaryValue = biggestPossibleUpperBoundary < midBetweenCurrentLevelMidAndUpperLevelMid ? biggestPossibleUpperBoundary + : midBetweenCurrentLevelMidAndUpperLevelMid; + } + upperBoundary = new ComparableClusterBoundary(new KnotSpeedImpl(upperBoundaryValue), + ComparisonStrategy.LOWER_THAN); + return upperBoundary; + } + + private static ClusterBoundary createLowerBoundary(double[] levelMidsInKnots, double maxDistanceInKnots, + int index) { + ClusterBoundary lowerBoundary; + double lowerBoundaryValue; + if (index > 0) { + double lowestPossibleLowerBoundary = levelMidsInKnots[index] - maxDistanceInKnots; + double midBetweenLowerLevelMidAndCurrentLevelMid = levelMidsInKnots[index - 1] + + (0.5 * (levelMidsInKnots[index] - levelMidsInKnots[index - 1])); + lowerBoundaryValue = lowestPossibleLowerBoundary > midBetweenLowerLevelMidAndCurrentLevelMid ? lowestPossibleLowerBoundary + : midBetweenLowerLevelMidAndCurrentLevelMid; + } else { + double lowestPossibleLowerBoundary = levelMidsInKnots[index] - maxDistanceInKnots; + lowerBoundaryValue = lowestPossibleLowerBoundary <= 0 ? 0 : lowestPossibleLowerBoundary; + } + lowerBoundary = new ComparableClusterBoundary(new KnotSpeedImpl(lowerBoundaryValue), + ComparisonStrategy.GREATER_EQUALS_THAN); + return lowerBoundary; + } + +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/BoatAndWindSpeedWithOriginInfo.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/BoatAndWindSpeedWithOriginInfo.java similarity index 81% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/BoatAndWindSpeedWithOriginInfo.java rename to java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/BoatAndWindSpeedWithOriginInfo.java index c3f537e45b4..7e10cb89af0 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/BoatAndWindSpeedWithOriginInfo.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/BoatAndWindSpeedWithOriginInfo.java @@ -1,4 +1,4 @@ -package com.sap.sailing.domain.polarsheets; +package com.sap.sailing.polars.data; import com.sap.sailing.domain.common.Speed; diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/DataPointWithOriginInfo.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/DataPointWithOriginInfo.java new file mode 100644 index 00000000000..9d8887e4064 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/DataPointWithOriginInfo.java @@ -0,0 +1,11 @@ +package com.sap.sailing.polars.data; + +public interface DataPointWithOriginInfo extends Comparable { + + public abstract Double getRawData(); + + public abstract String getWindGaugeIdString(); + + public abstract String getDayString(); + +} \ No newline at end of file diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/PolarFix.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/PolarFix.java new file mode 100644 index 00000000000..23f39d0e57d --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/PolarFix.java @@ -0,0 +1,18 @@ +package com.sap.sailing.polars.data; + +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; + +public interface PolarFix { + + public abstract SpeedWithBearing getBoatSpeed(); + + public abstract Speed getWindSpeed(); + + public abstract double getAngleToWind(); + + public abstract String getGaugeIdString(); + + public abstract String getDayString(); + +} \ No newline at end of file diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/BoatAndWindSpeedWithOriginInfoImpl.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/impl/BoatAndWindSpeedWithOriginInfoImpl.java similarity index 85% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/BoatAndWindSpeedWithOriginInfoImpl.java rename to java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/impl/BoatAndWindSpeedWithOriginInfoImpl.java index 38523f95363..d9d7cccd76f 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/BoatAndWindSpeedWithOriginInfoImpl.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/impl/BoatAndWindSpeedWithOriginInfoImpl.java @@ -1,6 +1,7 @@ -package com.sap.sailing.domain.polarsheets; +package com.sap.sailing.polars.data.impl; import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.polars.data.BoatAndWindSpeedWithOriginInfo; public class BoatAndWindSpeedWithOriginInfoImpl implements BoatAndWindSpeedWithOriginInfo { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/DataPointWithOriginInfo.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/impl/DataPointWithOriginInfoImpl.java similarity index 64% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/DataPointWithOriginInfo.java rename to java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/impl/DataPointWithOriginInfoImpl.java index a955bf1f6d3..a0fbe3af361 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/DataPointWithOriginInfo.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/data/impl/DataPointWithOriginInfoImpl.java @@ -1,6 +1,8 @@ -package com.sap.sailing.domain.polarsheets; +package com.sap.sailing.polars.data.impl; -public class DataPointWithOriginInfo implements Comparable{ +import com.sap.sailing.polars.data.DataPointWithOriginInfo; + +public class DataPointWithOriginInfoImpl implements DataPointWithOriginInfo { private Double rawData; @@ -8,20 +10,23 @@ public class DataPointWithOriginInfo implements Comparable track, Wind windSpeed, + public PolarFixImpl(GPSFixMoving fix, TrackedRace race, GPSFixTrack track, Wind windSpeed, PolarSheetGenerationSettings settings, String gaugeIdString) { boatSpeed = track.getEstimatedSpeed(fix.getTimePoint()); Bearing bearing = boatSpeed.getBearing(); @@ -93,22 +94,27 @@ public class PolarFix { return windSourcesToExclude; } + @Override public SpeedWithBearing getBoatSpeed() { return boatSpeed; } + @Override public Speed getWindSpeed() { return windSpeed; } + @Override public double getAngleToWind() { return angleToWind; } + @Override public String getGaugeIdString() { return gaugeIdString; } + @Override public String getDayString() { return dayString; } diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/factory/PolarDataServiceFactory.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/factory/PolarDataServiceFactory.java new file mode 100644 index 00000000000..0b03c20a553 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/factory/PolarDataServiceFactory.java @@ -0,0 +1,18 @@ +package com.sap.sailing.polars.factory; + +import java.util.concurrent.Executor; + +import com.sap.sailing.polars.PolarDataService; +import com.sap.sailing.polars.factory.impl.PolarDataServiceImplFactory; + +public abstract class PolarDataServiceFactory { + + public static PolarDataService createStandardPolarDataService(Executor executor) { + PolarDataServiceFactory factory = new PolarDataServiceImplFactory(); + return factory.createPolarDataService(executor); + } + + protected abstract PolarDataService createPolarDataService(Executor executor); + + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/factory/impl/PolarDataServiceImplFactory.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/factory/impl/PolarDataServiceImplFactory.java new file mode 100644 index 00000000000..37af01d8343 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/factory/impl/PolarDataServiceImplFactory.java @@ -0,0 +1,16 @@ +package com.sap.sailing.polars.factory.impl; + +import java.util.concurrent.Executor; + +import com.sap.sailing.polars.PolarDataService; +import com.sap.sailing.polars.factory.PolarDataServiceFactory; +import com.sap.sailing.polars.impl.PolarDataServiceImpl; + +public class PolarDataServiceImplFactory extends PolarDataServiceFactory { + + @Override + public PolarDataService createPolarDataService(Executor executor) { + return new PolarDataServiceImpl(executor); + } + +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/PolarSheetGenerationWorker.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/generation/PolarSheetGenerator.java similarity index 69% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/PolarSheetGenerationWorker.java rename to java/com.sap.sailing.polars/src/com/sap/sailing/polars/generation/PolarSheetGenerator.java index 8d56adaf30c..ef920e65ceb 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/PolarSheetGenerationWorker.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/generation/PolarSheetGenerator.java @@ -1,387 +1,277 @@ -package com.sap.sailing.domain.polarsheets; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import com.sap.sailing.domain.base.Competitor; -import com.sap.sailing.domain.base.RaceDefinition; -import com.sap.sailing.domain.common.PolarSheetGenerationSettings; -import com.sap.sailing.domain.common.PolarSheetsData; -import com.sap.sailing.domain.common.PolarSheetsHistogramData; -import com.sap.sailing.domain.common.Speed; -import com.sap.sailing.domain.common.impl.PolarSheetsDataImpl; -import com.sap.sailing.domain.common.impl.WindSteppingWithMaxDistance; -import com.sap.sailing.domain.tracking.TrackedRace; -import com.sap.sse.common.TimePoint; - -/** - * Allows extracting data about average speed for each angle creating the foundation of polar-sheet generation. Tasks - * are assigned to the executor per race & per competitor and the results are filled on the go to allow progress - * indication in the polar-sheet itself. - * - * @author D054528 Frederik Petersen - * - */ -public class PolarSheetGenerationWorker implements Future{ - - private final Set workers; - - private final List> polarData; - - private final Executor executor; - - private WindSteppingWithMaxDistance stepping; - - private PolarSheetGenerationSettings settings; - - /** - * Will prepare the {@link PerRaceAndCompetitorPolarSheetGenerationWorker}s per race & per competitor. This includes - * determining start and end time. - * - * @param trackedRaces - * from which the data is to be collected - * @param settings - * @param executor - * executes the tasks upon {@link #startPolarSheetGeneration()} - */ - public PolarSheetGenerationWorker(Set trackedRaces, PolarSheetGenerationSettings settings, - Executor executor) { - this.settings = settings; - stepping = settings.getWindStepping();; - polarData = initializePolarDataContainer(); - this.executor = executor; - workers = new HashSet(); - for (TrackedRace race : trackedRaces) { - TimePoint startTime = race.getStartOfRace(); - TimePoint endTime = race.getEndOfRace(); - if (endTime == null) { - endTime = race.getTimePointOfNewestEvent(); - } - RaceDefinition raceDefinition = race.getRace(); - Iterable competitors = raceDefinition.getCompetitors(); - - for (Competitor competitor : competitors) { - PerRaceAndCompetitorPolarSheetGenerationWorker task = new PerRaceAndCompetitorPolarSheetGenerationWorker( - race, this, startTime, endTime, competitor, settings); - workers.add(task); - } - - } - } - - private List> initializePolarDataContainer() { - List> container = new ArrayList>(); - for (int i = 0; i < 360; i++) { - container.add(new ArrayList()); - } - return container; - } - - /** - * Starts the {@link PerRaceAndCompetitorPolarSheetGenerationWorker}s - */ - public void startPolarSheetGeneration() { - for (Runnable task : workers) { - executor.execute(task); - } - } - - /** - * To be called from {@link PerRaceAndCompetitorPolarSheetGenerationWorker} for adding a datapoint to the list of - * results - * - * @param roundedAngle - * boat's angle to the wind - * @param boatSpeed - * boat's speed in knots - * @param windSpeed - * wind's speed in knots - */ - protected void addPolarData(PolarFix polarFix) { - long roundedAngle = Math.round(polarFix.getAngleToWind()); - Speed boatSpeed = polarFix.getBoatSpeed(); - Speed windSpeed = polarFix.getWindSpeed(); - int angle = (int) roundedAngle; - if (angle < 0) { - angle = (360 + angle); - } - BoatAndWindSpeedWithOriginInfo speeds = new BoatAndWindSpeedWithOriginInfoImpl(boatSpeed, windSpeed, - polarFix.getGaugeIdString(), polarFix.getDayString()); - polarData.get(angle).add(speeds); - } - - /** - * - * @return results already averaged per angle. Also datacount overall and per angle. And a completion flag to show - * if the complete polar sheet has been generated or if the current results are only an intermediate - * product. - */ - public PolarSheetsData getPolarData() { - - // Do this before working through data, since it's possible that remaining data is processed by the workers - // during the process of filling the result data. - boolean complete = allWorkersDone(); - - PolarSheetHistogramBuilder histogramBuilder = new PolarSheetHistogramBuilder(settings); - - int levelCount = stepping.getNumberOfLevels(); - Number[][] averagedPolarDataByWindSpeed = new Number[levelCount][360]; - Integer[] dataCountPerAngle = new Integer[360]; - int dataCount = 0; - Map dataCountPerAngleForWindspeed = new HashMap(); - Map> histogramDataMap = new HashMap>(); - Map totalDataCountPerWindSpeed = new HashMap(); - for (int levelIndex = 0; levelIndex < levelCount; levelIndex++) { - dataCountPerAngleForWindspeed.put(levelIndex, new Integer[360]); - histogramDataMap.put(levelIndex, new HashMap()); - totalDataCountPerWindSpeed.put(levelIndex, 0); - } - for (int angleIndex = 0; angleIndex < 360; angleIndex++) { - // Avoid Concurrent modification of lists, so get current state as an array; lock would slow things down - BoatAndWindSpeedWithOriginInfo[] values = polarData.get(angleIndex).toArray( - new BoatAndWindSpeedWithOriginInfo[polarData.get(angleIndex).size()]); - dataCount = dataCount + values.length; - dataCountPerAngle[angleIndex] = values.length; - double[] sumsPerWindSpeed = new double[levelCount]; - int[] dataCountPerWindSpeed = new int[levelCount]; - Map> dataSetForWindLevel = new HashMap>(); - gatherPolarData(values, sumsPerWindSpeed, dataCountPerWindSpeed, dataSetForWindLevel); - - calculateStatisticalMeasuresAndHistogramData(histogramBuilder, levelCount, averagedPolarDataByWindSpeed, - dataCountPerAngleForWindspeed, histogramDataMap, totalDataCountPerWindSpeed, angleIndex, - sumsPerWindSpeed, dataCountPerWindSpeed, dataSetForWindLevel); - - } - - for (int j = 0; j < levelCount; j++) { - for (PolarSheetsHistogramData histogramData : histogramDataMap.get(j).values()) { - Double polarSheetPointConfidenceMeasure = Math.min(1, histogramData.getCoefficiantOfVariation() - * (histogramData.getDataCount() / (double) totalDataCountPerWindSpeed.get(j)) * 80); - histogramData.setConfidenceMeasure(polarSheetPointConfidenceMeasure); - } - } - - PolarSheetsData data = new PolarSheetsDataImpl(averagedPolarDataByWindSpeed, complete, dataCount, - dataCountPerAngleForWindspeed, stepping, histogramDataMap); - - return data; - } - - private void gatherPolarData(BoatAndWindSpeedWithOriginInfo[] values, double[] sumsPerWindSpeed, - int[] dataCountPerWindSpeed, Map> dataSetForWindLevel) { - for (BoatAndWindSpeedWithOriginInfo singleDataPoint : values) { - if (singleDataPoint != null) { - double windSpeed = singleDataPoint.getWindSpeed().getKnots(); - int level = stepping.getLevelIndexForValue(windSpeed); - if (level >= 0) { - double speed = singleDataPoint.getBoatSpeed().getKnots(); - String windGaugesIdString = singleDataPoint.getWindGaugesIdString(); - String dayString = singleDataPoint.getDayString(); - if (!dataSetForWindLevel.containsKey(level)) { - dataSetForWindLevel.put(level, new ArrayList()); - } - dataSetForWindLevel.get(level).add( - new DataPointWithOriginInfo(speed, windGaugesIdString, dayString)); - sumsPerWindSpeed[level] = sumsPerWindSpeed[level] + speed; - dataCountPerWindSpeed[level]++; - } - } - } - } - - private void calculateStatisticalMeasuresAndHistogramData(PolarSheetHistogramBuilder histogramBuilder, - int levelCount, Number[][] averagedPolarDataByWindSpeed, - Map dataCountPerAngleForWindspeed, - Map> histogramDataMap, - Map totalDataCountPerWindSpeed, int angleIndex, double[] sumsPerWindSpeed, - int[] dataCountPerWindSpeed, Map> dataSetForWindLevel) { - for (int levelIndex = 0; levelIndex < levelCount; levelIndex++) { - if (settings.shouldRemoveOutliers() && dataSetForWindLevel.get(levelIndex) != null) { - List withoutOutliers = new ArrayList(); - Collections.sort(dataSetForWindLevel.get(levelIndex)); - if (dataCountPerWindSpeed[levelIndex] > /* Minimum data count for outlier detection */10) { - performOutlierExclusion(sumsPerWindSpeed, dataCountPerWindSpeed, dataSetForWindLevel, - levelIndex, withoutOutliers); - } - - } - - if (dataCountPerWindSpeed[levelIndex] < settings.getMinimumDataCountPerAngle()) { - dataSetForWindLevel.put(levelIndex, new ArrayList()); - sumsPerWindSpeed[levelIndex] = 0; - dataCountPerWindSpeed[levelIndex] = 0; - } - totalDataCountPerWindSpeed.put(levelIndex, totalDataCountPerWindSpeed.get(levelIndex) - + dataCountPerWindSpeed[levelIndex]); - Double average = sumsPerWindSpeed[levelIndex] / dataCountPerWindSpeed[levelIndex]; - if (average.isNaN()) { - average = new Double(0); - } - averagedPolarDataByWindSpeed[levelIndex][angleIndex] = average; - dataCountPerAngleForWindspeed.get(levelIndex)[angleIndex] = dataCountPerWindSpeed[levelIndex]; - double coefficiantOfVariation = 0; - if (dataCountPerWindSpeed[levelIndex] > 0 && average > 0) { - Double variance = sumsPerWindSpeed[levelIndex] / dataCountPerWindSpeed[levelIndex]; - Double standardDeviation = Math.sqrt(variance); - coefficiantOfVariation = standardDeviation / average; - } - List rawData = dataSetForWindLevel.get(levelIndex); - if (rawData == null || rawData.size() <= 0) { - continue; - } - PolarSheetsHistogramData histogramData = histogramBuilder.build(rawData, angleIndex, - coefficiantOfVariation); - histogramDataMap.get(levelIndex).put(angleIndex, histogramData); - - } - } - - private void performOutlierExclusion(double[] sumsPerWindSpeed, int[] dataCountPerWindSpeed, - Map> dataSetForWindLevel, int levelIndex, - List withoutOutliers) { - for (int dataIndex = 0; dataIndex < dataSetForWindLevel.get(levelIndex).size(); dataIndex++) { - DataPointWithOriginInfo dataPoint = dataSetForWindLevel.get(levelIndex).get(dataIndex); - double pct = getNeighboorhoodSizePercentage(dataCountPerWindSpeed, dataSetForWindLevel, - levelIndex, dataIndex, dataPoint, settings); - if (pct >= settings.getOutlierMinimumNeighborhoodPct()) { - withoutOutliers.add(dataPoint); - } else { - sumsPerWindSpeed[levelIndex] = sumsPerWindSpeed[levelIndex] - dataPoint.getRawData(); - dataCountPerWindSpeed[levelIndex]--; - } - } - dataSetForWindLevel.put(levelIndex, withoutOutliers); - } - - /** - * This method calculates the pct of the points neighborhoods based on a distance (in the settings) compared - * to the complete dataset. - */ - public static double getNeighboorhoodSizePercentage(int[] dataCountPerWindSpeed, - Map> dataSetForWindLevel, int levelIndex, int dataIndex, - DataPointWithOriginInfo dataPoint, PolarSheetGenerationSettings settings) { - int neighborCount = 0; - double pct = .0; - - int index = dataIndex; - // Left side - boolean done = false; - while (!done) { - index--; - if (index < 0) { - done = true; - } else { - Double dataPointToCheckForNeighborStatus = dataSetForWindLevel.get(levelIndex).get( - index).getRawData(); - if (dataPoint.getRawData() - dataPointToCheckForNeighborStatus <= settings - .getOutlierDetectionNeighborhoodRadius()) { - neighborCount++; - } else { - done = true; - } - pct = (double) neighborCount / (double) dataCountPerWindSpeed[levelIndex]; - if (pct >= settings.getOutlierMinimumNeighborhoodPct()) { - done = true; - } - } - } - index = dataIndex; - // Right side - done = false; - while (!done) { - index++; - if (index >= dataCountPerWindSpeed[levelIndex]) { - done = true; - } else { - Double dataPointToCheckForNeighborStatus = dataSetForWindLevel.get(levelIndex).get( - index).getRawData(); - if (dataPointToCheckForNeighborStatus - dataPoint.getRawData() <= settings - .getOutlierDetectionNeighborhoodRadius()) { - neighborCount++; - } else { - done = true; - } - pct = (double) neighborCount / (double) dataCountPerWindSpeed[levelIndex]; - if (pct >= settings.getOutlierMinimumNeighborhoodPct()) { - done = true; - } - } - } - pct = (double) neighborCount / (double) dataCountPerWindSpeed[levelIndex]; - return pct; - } - - private boolean allWorkersDone() { - boolean complete = true; - for (PerRaceAndCompetitorPolarSheetGenerationWorker task : workers) { - if (!task.isDone()) { - complete = false; - break; - } - } - return complete; - } - - /** - * Can be used to present more detailed data. - * - * @return The complete set of datapoints that have been added. Can be quite big, depending on the amount of races - * and competitors. - */ - public List> getCompleteData() { - return polarData; - } - - public WindSteppingWithMaxDistance getStepping() { - return stepping; - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - return false; - } - - @Override - public boolean isCancelled() { - return false; - } - - @Override - public boolean isDone() { - return allWorkersDone(); - } - - @Override - public PolarSheetsData get() throws InterruptedException, ExecutionException { - while (!isDone()) { - Thread.sleep(100); - } - return getPolarData(); - } - - @Override - public PolarSheetsData get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, - TimeoutException { - long timeRun = 0; - long timeoutInMillis = unit.toMillis(timeout); - while (!isDone() && timeRun < timeoutInMillis) { - Thread.sleep(100); - timeRun = timeRun + 100; - } - if (timeRun >= timeoutInMillis) { - throw new TimeoutException(); - } - return getPolarData(); - } - -} +package com.sap.sailing.polars.generation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.sap.sailing.domain.common.PolarSheetGenerationSettings; +import com.sap.sailing.domain.common.PolarSheetsData; +import com.sap.sailing.domain.common.PolarSheetsHistogramData; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.impl.PolarSheetsDataImpl; +import com.sap.sailing.domain.common.impl.WindSteppingWithMaxDistance; +import com.sap.sailing.polars.data.BoatAndWindSpeedWithOriginInfo; +import com.sap.sailing.polars.data.DataPointWithOriginInfo; +import com.sap.sailing.polars.data.PolarFix; +import com.sap.sailing.polars.data.impl.BoatAndWindSpeedWithOriginInfoImpl; +import com.sap.sailing.polars.data.impl.DataPointWithOriginInfoImpl; + +public class PolarSheetGenerator { + + private final List> polarData; + + private WindSteppingWithMaxDistance stepping; + + private PolarSheetGenerationSettings settings; + + private final Set fixes; + + public PolarSheetGenerator(Set fixes, PolarSheetGenerationSettings settings) { + this.settings = settings; + this.fixes = fixes; + stepping = settings.getWindStepping(); + polarData = initializePolarDataContainer(); + } + + private List> initializePolarDataContainer() { + List> container = new ArrayList>(); + for (int i = 0; i < 360; i++) { + container.add(new ArrayList()); + } + return container; + } + + + private void addPolarFix(PolarFix polarFix) { + long roundedAngle = Math.round(polarFix.getAngleToWind()); + Speed boatSpeed = polarFix.getBoatSpeed(); + Speed windSpeed = polarFix.getWindSpeed(); + int angle = (int) roundedAngle; + if (angle < 0) { + angle = (360 + angle); + } + BoatAndWindSpeedWithOriginInfo speeds = new BoatAndWindSpeedWithOriginInfoImpl(boatSpeed, windSpeed, + polarFix.getGaugeIdString(), polarFix.getDayString()); + polarData.get(angle).add(speeds); + } + + /** + * + * @return results already averaged per angle. Also datacount overall and per angle. And a completion flag to show + * if the complete polar sheet has been generated or if the current results are only an intermediate + * product. + */ + public PolarSheetsData generate() { + + for (PolarFix fix : fixes) { + addPolarFix(fix); + } + + PolarSheetHistogramBuilder histogramBuilder = new PolarSheetHistogramBuilder(settings); + + int levelCount = stepping.getNumberOfLevels(); + Number[][] averagedPolarDataByWindSpeed = new Number[levelCount][360]; + Integer[] dataCountPerAngle = new Integer[360]; + int dataCount = 0; + Map dataCountPerAngleForWindspeed = new HashMap(); + Map> histogramDataMap = new HashMap>(); + Map totalDataCountPerWindSpeed = new HashMap(); + for (int levelIndex = 0; levelIndex < levelCount; levelIndex++) { + dataCountPerAngleForWindspeed.put(levelIndex, new Integer[360]); + histogramDataMap.put(levelIndex, new HashMap()); + totalDataCountPerWindSpeed.put(levelIndex, 0); + } + for (int angleIndex = 0; angleIndex < 360; angleIndex++) { + // Avoid Concurrent modification of lists, so get current state as an array; lock would slow things down + BoatAndWindSpeedWithOriginInfo[] values = polarData.get(angleIndex).toArray( + new BoatAndWindSpeedWithOriginInfo[polarData.get(angleIndex).size()]); + dataCount = dataCount + values.length; + dataCountPerAngle[angleIndex] = values.length; + double[] sumsPerWindSpeed = new double[levelCount]; + int[] dataCountPerWindSpeed = new int[levelCount]; + Map> dataSetForWindLevel = new HashMap>(); + gatherPolarData(values, sumsPerWindSpeed, dataCountPerWindSpeed, dataSetForWindLevel); + + calculateStatisticalMeasuresAndHistogramData(histogramBuilder, levelCount, averagedPolarDataByWindSpeed, + dataCountPerAngleForWindspeed, histogramDataMap, totalDataCountPerWindSpeed, angleIndex, + sumsPerWindSpeed, dataCountPerWindSpeed, dataSetForWindLevel); + + } + + for (int j = 0; j < levelCount; j++) { + for (PolarSheetsHistogramData histogramData : histogramDataMap.get(j).values()) { + Double polarSheetPointConfidenceMeasure = Math.min(1, histogramData.getCoefficiantOfVariation() + * (histogramData.getDataCount() / (double) totalDataCountPerWindSpeed.get(j)) * 80); + histogramData.setConfidenceMeasure(polarSheetPointConfidenceMeasure); + } + } + + PolarSheetsData data = new PolarSheetsDataImpl(averagedPolarDataByWindSpeed, dataCount, + dataCountPerAngleForWindspeed, stepping, histogramDataMap); + + return data; + } + + private void gatherPolarData(BoatAndWindSpeedWithOriginInfo[] values, double[] sumsPerWindSpeed, + int[] dataCountPerWindSpeed, Map> dataSetForWindLevel) { + for (BoatAndWindSpeedWithOriginInfo singleDataPoint : values) { + if (singleDataPoint != null) { + double windSpeed = singleDataPoint.getWindSpeed().getKnots(); + int level = stepping.getLevelIndexForValue(windSpeed); + if (level >= 0) { + double speed = singleDataPoint.getBoatSpeed().getKnots(); + String windGaugesIdString = singleDataPoint.getWindGaugesIdString(); + String dayString = singleDataPoint.getDayString(); + if (!dataSetForWindLevel.containsKey(level)) { + dataSetForWindLevel.put(level, new ArrayList()); + } + dataSetForWindLevel.get(level).add( + new DataPointWithOriginInfoImpl(speed, windGaugesIdString, dayString)); + sumsPerWindSpeed[level] = sumsPerWindSpeed[level] + speed; + dataCountPerWindSpeed[level]++; + } + } + } + } + + private void calculateStatisticalMeasuresAndHistogramData(PolarSheetHistogramBuilder histogramBuilder, + int levelCount, Number[][] averagedPolarDataByWindSpeed, + Map dataCountPerAngleForWindspeed, + Map> histogramDataMap, + Map totalDataCountPerWindSpeed, int angleIndex, double[] sumsPerWindSpeed, + int[] dataCountPerWindSpeed, Map> dataSetForWindLevel) { + for (int levelIndex = 0; levelIndex < levelCount; levelIndex++) { + if (settings.shouldRemoveOutliers() && dataSetForWindLevel.get(levelIndex) != null) { + List withoutOutliers = new ArrayList(); + Collections.sort(dataSetForWindLevel.get(levelIndex)); + if (dataCountPerWindSpeed[levelIndex] > /* Minimum data count for outlier detection */10) { + performOutlierExclusion(sumsPerWindSpeed, dataCountPerWindSpeed, dataSetForWindLevel, + levelIndex, withoutOutliers); + } + + } + + if (dataCountPerWindSpeed[levelIndex] < settings.getMinimumDataCountPerAngle()) { + dataSetForWindLevel.put(levelIndex, new ArrayList()); + sumsPerWindSpeed[levelIndex] = 0; + dataCountPerWindSpeed[levelIndex] = 0; + } + totalDataCountPerWindSpeed.put(levelIndex, totalDataCountPerWindSpeed.get(levelIndex) + + dataCountPerWindSpeed[levelIndex]); + Double average = sumsPerWindSpeed[levelIndex] / dataCountPerWindSpeed[levelIndex]; + if (average.isNaN()) { + average = new Double(0); + } + averagedPolarDataByWindSpeed[levelIndex][angleIndex] = average; + dataCountPerAngleForWindspeed.get(levelIndex)[angleIndex] = dataCountPerWindSpeed[levelIndex]; + double coefficiantOfVariation = 0; + if (dataCountPerWindSpeed[levelIndex] > 0 && average > 0) { + Double variance = sumsPerWindSpeed[levelIndex] / dataCountPerWindSpeed[levelIndex]; + Double standardDeviation = Math.sqrt(variance); + coefficiantOfVariation = standardDeviation / average; + } + List rawData = dataSetForWindLevel.get(levelIndex); + if (rawData == null || rawData.size() <= 0) { + continue; + } + PolarSheetsHistogramData histogramData = histogramBuilder.build(rawData, angleIndex, + coefficiantOfVariation); + histogramDataMap.get(levelIndex).put(angleIndex, histogramData); + + } + } + + private void performOutlierExclusion(double[] sumsPerWindSpeed, int[] dataCountPerWindSpeed, + Map> dataSetForWindLevel, int levelIndex, + List withoutOutliers) { + for (int dataIndex = 0; dataIndex < dataSetForWindLevel.get(levelIndex).size(); dataIndex++) { + DataPointWithOriginInfo dataPoint = dataSetForWindLevel.get(levelIndex).get(dataIndex); + double pct = getNeighboorhoodSizePercentage(dataCountPerWindSpeed, dataSetForWindLevel, + levelIndex, dataIndex, dataPoint, settings); + if (pct >= settings.getOutlierMinimumNeighborhoodPct()) { + withoutOutliers.add(dataPoint); + } else { + sumsPerWindSpeed[levelIndex] = sumsPerWindSpeed[levelIndex] - dataPoint.getRawData(); + dataCountPerWindSpeed[levelIndex]--; + } + } + dataSetForWindLevel.put(levelIndex, withoutOutliers); + } + + /** + * This method calculates the pct of the points neighborhoods based on a distance (in the settings) compared + * to the complete dataset. + */ + public static double getNeighboorhoodSizePercentage(int[] dataCountPerWindSpeed, + Map> valuesInMap, int levelIndex, int dataIndex, + DataPointWithOriginInfo dataPoint, PolarSheetGenerationSettings settings) { + int neighborCount = 0; + double pct = .0; + + int index = dataIndex; + // Left side + boolean done = false; + while (!done) { + index--; + if (index < 0) { + done = true; + } else { + Double dataPointToCheckForNeighborStatus = valuesInMap.get(levelIndex).get( + index).getRawData(); + if (dataPoint.getRawData() - dataPointToCheckForNeighborStatus <= settings + .getOutlierDetectionNeighborhoodRadius()) { + neighborCount++; + } else { + done = true; + } + pct = (double) neighborCount / (double) dataCountPerWindSpeed[levelIndex]; + if (pct >= settings.getOutlierMinimumNeighborhoodPct()) { + done = true; + } + } + } + index = dataIndex; + // Right side + done = false; + while (!done) { + index++; + if (index >= dataCountPerWindSpeed[levelIndex]) { + done = true; + } else { + Double dataPointToCheckForNeighborStatus = valuesInMap.get(levelIndex).get( + index).getRawData(); + if (dataPointToCheckForNeighborStatus - dataPoint.getRawData() <= settings + .getOutlierDetectionNeighborhoodRadius()) { + neighborCount++; + } else { + done = true; + } + pct = (double) neighborCount / (double) dataCountPerWindSpeed[levelIndex]; + if (pct >= settings.getOutlierMinimumNeighborhoodPct()) { + done = true; + } + } + } + pct = (double) neighborCount / (double) dataCountPerWindSpeed[levelIndex]; + return pct; + } + + + /** + * Can be used to present more detailed data. + * + * @return The complete set of datapoints that have been added. Can be quite big, depending on the amount of races + * and competitors. + */ + public List> getCompleteData() { + return polarData; + } + + public WindSteppingWithMaxDistance getStepping() { + return stepping; + } + +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/PolarSheetHistogramBuilder.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/generation/PolarSheetHistogramBuilder.java similarity index 88% rename from java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/PolarSheetHistogramBuilder.java rename to java/com.sap.sailing.polars/src/com/sap/sailing/polars/generation/PolarSheetHistogramBuilder.java index ca980475494..fdbbe434675 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/polarsheets/PolarSheetHistogramBuilder.java +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/generation/PolarSheetHistogramBuilder.java @@ -1,4 +1,4 @@ -package com.sap.sailing.domain.polarsheets; +package com.sap.sailing.polars.generation; import java.util.Collections; import java.util.HashMap; @@ -8,6 +8,7 @@ import java.util.Map; import com.sap.sailing.domain.common.PolarSheetGenerationSettings; import com.sap.sailing.domain.common.PolarSheetsHistogramData; import com.sap.sailing.domain.common.impl.PolarSheetsHistogramDataImpl; +import com.sap.sailing.polars.data.DataPointWithOriginInfo; public class PolarSheetHistogramBuilder { @@ -17,10 +18,10 @@ public class PolarSheetHistogramBuilder { this.settings = settings; } - public PolarSheetsHistogramData build(List dataPointsWithOriginInfo, int angleIndex, + public PolarSheetsHistogramData build(List rawData, int angleIndex, double coefficiantOfVariation) { - DataPointWithOriginInfo minPoint = Collections.min(dataPointsWithOriginInfo); - DataPointWithOriginInfo maxPoint = Collections.max(dataPointsWithOriginInfo); + DataPointWithOriginInfo minPoint = Collections.min(rawData); + DataPointWithOriginInfo maxPoint = Collections.max(rawData); Double min = minPoint.getRawData(); Double max = maxPoint.getRawData(); @@ -39,7 +40,7 @@ public class PolarSheetHistogramBuilder { for (int i = 0; i < yValues.length; i++) { yValues[i] = 0; } - for (DataPointWithOriginInfo dataPoint : dataPointsWithOriginInfo) { + for (DataPointWithOriginInfo dataPoint : rawData) { double notRoundedYet = (dataPoint.getRawData() - min) / range; int u = (int) notRoundedYet; if (u == numberOfColumns) { @@ -61,7 +62,7 @@ public class PolarSheetHistogramBuilder { PolarSheetsHistogramData histogramData = new PolarSheetsHistogramDataImpl(angleIndex, xValues, yValues, - yValuesByGaugeIds, yValuesByDay, yValuesByDayAndGaugeId, dataPointsWithOriginInfo.size(), coefficiantOfVariation); + yValuesByGaugeIds, yValuesByDay, yValuesByDayAndGaugeId, rawData.size(), coefficiantOfVariation); return histogramData; } diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/PolarDataServiceImpl.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/PolarDataServiceImpl.java new file mode 100644 index 00000000000..d669092b27d --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/impl/PolarDataServiceImpl.java @@ -0,0 +1,179 @@ +package com.sap.sailing.polars.impl; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.PolarSheetGenerationSettings; +import com.sap.sailing.domain.common.PolarSheetsData; +import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.polars.PolarDataService; +import com.sap.sailing.polars.aggregation.PolarFixAggregator; +import com.sap.sailing.polars.aggregation.SimplePolarFixRaceInterval; +import com.sap.sailing.polars.analysis.PolarSheetAnalyzer; +import com.sap.sailing.polars.analysis.impl.PolarSheetAnalyzerImpl; +import com.sap.sailing.polars.caching.PolarFixCache; +import com.sap.sailing.polars.caching.PolarSheetPerBoatClassCache; +import com.sap.sailing.polars.data.PolarFix; +import com.sap.sailing.polars.generation.PolarSheetGenerator; +import com.sap.sailing.polars.mining.PolarDataMiner; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; +import com.sap.sailing.util.SmartFutureCache; + +/** + * Uses two chained {@link SmartFutureCache}s. One to store {@link PolarFix}es extracted from {@link TrackedRace}s and + * the other one for storing one polar sheet per boat class. This allows quick access to desired measures like optimal + * beat angles. + * + * @author Frederik Petersen (D054528) + * + */ +public class PolarDataServiceImpl implements PolarDataService { + + private final PolarFixCache polarFixCache; + private final PolarSheetPerBoatClassCache polarSheetPerBoatClassCache; + + private final PolarSheetAnalyzer polarSheetAnalyzer; + + private final PolarDataMiner polarDataMiner; + + public PolarDataServiceImpl(Executor executor) { + this.polarFixCache = new PolarFixCache(executor); + this.polarSheetPerBoatClassCache = new PolarSheetPerBoatClassCache(this); + polarFixCache.addListener(polarSheetPerBoatClassCache); + this.polarSheetAnalyzer = new PolarSheetAnalyzerImpl(this); + this.polarDataMiner = new PolarDataMiner(); + } + + @Override + public SpeedWithConfidence getSpeed(BoatClass boatClass, Speed windSpeed, Bearing bearingToTheWind, + boolean useLinearRegression) throws NotEnoughDataHasBeenAddedException { + return polarDataMiner.estimateBoatSpeed(boatClass, windSpeed, bearingToTheWind, useLinearRegression); + } + + @Override + public SpeedWithBearingWithConfidence getAverageSpeedWithBearing(BoatClass boatClass, Speed windSpeed, + LegType legType, Tack tack) throws NotEnoughDataHasBeenAddedException { + return getAverageSpeedWithBearing(boatClass, windSpeed, legType, tack, true); + } + + @Override + public SpeedWithBearingWithConfidence getAverageSpeedWithBearing(BoatClass boatClass, + Speed windSpeed, LegType legType, Tack tack, boolean useLinReg) throws NotEnoughDataHasBeenAddedException { + SpeedWithBearingWithConfidence speedWithBearing = null; + if (tack.equals(Tack.STARBOARD)) { + if (legType.equals(LegType.UPWIND)) { + speedWithBearing = polarSheetAnalyzer.getAverageUpwindSpeedWithBearingOnStarboardTackFor(boatClass, windSpeed, useLinReg); + } else if (legType.equals(LegType.DOWNWIND)) { + speedWithBearing = polarSheetAnalyzer.getAverageDownwindSpeedWithBearingOnStarboardTackFor(boatClass, windSpeed, useLinReg); + } + } else if (tack.equals(Tack.PORT)) { + if (legType.equals(LegType.UPWIND)) { + return polarSheetAnalyzer.getAverageUpwindSpeedWithBearingOnPortTackFor(boatClass, windSpeed, useLinReg); + } else if (legType.equals(LegType.DOWNWIND)) { + return polarSheetAnalyzer.getAverageDownwindSpeedWithBearingOnPortTackFor(boatClass, windSpeed, useLinReg); + } + } + return speedWithBearing; + } + + + @Override + public PolarSheetsData generatePolarSheet(Set trackedRaces, PolarSheetGenerationSettings settings, + Executor executor) throws InterruptedException, ExecutionException { + Set fixes; + // If settings are default, look into cache + // if (settings.areDefault()) { + // try { + // fixes = polarFixCache.getFixesForTrackedRaces(trackedRaces); + // } catch (NoCacheEntryException e) { + // // If there is no cache entry for at least one of the races: Aggregate for non-cached races. + // fixes = e.getCachedResultList(); + // PolarFixAggregator aggregator = new PolarFixAggregator( + // new SimplePolarFixRaceInterval(e.getNotCached()), + // settings, executor); + // aggregator.startPolarFixAggregation(); + // fixes.addAll(aggregator.getAggregationResultAsSingleList()); + // } + // } else { + PolarFixAggregator aggregator = new PolarFixAggregator(new SimplePolarFixRaceInterval(trackedRaces), settings, + executor); + aggregator.startPolarFixAggregation(); + fixes = aggregator.getAggregationResultAsSingleList(); + // } + PolarSheetGenerator generator = new PolarSheetGenerator(fixes, settings); + return generator.generate(); + } + + @Override + public void newRaceFinishedTracking(TrackedRace trackedRace) { + // TODO add a similar listening method, that deletes all cached data on removal of race + // HashSet set = new HashSet(); + // set.add(trackedRace); + // polarFixCache.triggerUpdate(trackedRace.getRace().getBoatClass(), new PolarFixCacheRaceInterval(set)); + } + + @Override + public Set getPolarFixesForBoatClass(BoatClass key) { + Map> fixesInMap = polarFixCache.get(key, false); + Set resultSet = new HashSet(); + for (List value : fixesInMap.values()) { + resultSet.addAll(value); + } + return resultSet; + } + + @Override + public PolarSheetsData getPolarSheetForBoatClass(BoatClass boatClass) { + return polarDataMiner.createFullSheetForBoatClass(boatClass); + //return polarSheetPerBoatClassCache.get(boatClass, false); + } + + @Override + public Set getAllBoatClassesWithPolarSheetsAvailable() { + return polarDataMiner.getAvailableBoatClasses(); + //return polarSheetPerBoatClassCache.keySet(); + } + + @Override + public void competitorPositionChanged(final GPSFixMoving fix, final Competitor competitor, + final TrackedRace createdTrackedRace) { + polarDataMiner.addFix(fix, competitor, createdTrackedRace); + // TODO build datamining pipeline for the cache + + // PolarFixCacheRaceInterval interval = new PolarFixCacheRaceInterval(createIntervalSpecification( + // createdTrackedRace, competitor, timePoint)); + // polarFixCache.triggerUpdate(createdTrackedRace.getRace().getBoatClass(), interval); + + } + + @Override + public Integer[] getDataCountsForWindSpeed(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive) { + return polarDataMiner.getDataCountsForWindSpeed(boatClass, windSpeed, startAngleInclusive, endAngleExclusive); + } + + // private Map>> createIntervalSpecification( + // TrackedRace createdTrackedRace, Competitor competitor, TimePoint timePoint) { + // HashMap>> result = new HashMap>>(); + // HashMap> competitorMap = new HashMap>(); + // competitorMap.put(competitor, new Pair(timePoint, timePoint)); + // result.put(createdTrackedRace, competitorMap); + // return result; + // } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/AbstractEnrichingProcessor.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/AbstractEnrichingProcessor.java new file mode 100644 index 00000000000..67fc27b49b6 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/AbstractEnrichingProcessor.java @@ -0,0 +1,36 @@ +package com.sap.sailing.polars.mining; + +import java.util.Collection; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; + +import com.sap.sse.datamining.AdditionalResultDataBuilder; +import com.sap.sse.datamining.components.Processor; +import com.sap.sse.datamining.impl.components.AbstractSimpleParallelProcessor; + +public abstract class AbstractEnrichingProcessor extends + AbstractSimpleParallelProcessor { + + public AbstractEnrichingProcessor(Class inputType, Class resultType, + ExecutorService executor, Collection> resultReceivers) { + super(inputType, resultType, executor, resultReceivers); + } + + @Override + protected Callable createInstruction(final InputType element) { + return new Callable() { + + @Override + public ResultType call() throws Exception { + return enrich(element); + } + }; + } + + protected abstract ResultType enrich(InputType element); + + @Override + protected void setAdditionalData(AdditionalResultDataBuilder additionalDataBuilder) { + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/GPSFixMovingWithOriginInfo.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/GPSFixMovingWithOriginInfo.java new file mode 100644 index 00000000000..b3b90ed4bad --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/GPSFixMovingWithOriginInfo.java @@ -0,0 +1,33 @@ +package com.sap.sailing.polars.mining; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.TrackedRace; + +public class GPSFixMovingWithOriginInfo { + + private final GPSFixMoving fix; + private final TrackedRace trackedRace; + private final Competitor competitor; + + public GPSFixMovingWithOriginInfo(GPSFixMoving fix, TrackedRace trackedRace, Competitor competitor) { + this.fix = fix; + this.trackedRace = trackedRace; + this.competitor = competitor; + } + + public GPSFixMoving getFix() { + return fix; + } + + public TrackedRace getTrackedRace() { + return trackedRace; + } + + public Competitor getCompetitor() { + return competitor; + } + + + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/GPSFixMovingWithPolarContext.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/GPSFixMovingWithPolarContext.java new file mode 100644 index 00000000000..c60cd3f04b7 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/GPSFixMovingWithPolarContext.java @@ -0,0 +1,151 @@ +package com.sap.sailing.polars.mining; + +import java.util.HashSet; +import java.util.Set; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.WindSource; +import com.sap.sailing.domain.common.WindSourceType; +import com.sap.sailing.domain.common.confidence.BearingWithConfidence; +import com.sap.sailing.domain.common.confidence.ConfidenceFactory; +import com.sap.sailing.domain.common.confidence.impl.BearingWithConfidenceImpl; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.domain.tracking.WindWithConfidence; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.datamining.data.Cluster; +import com.sap.sse.datamining.data.ClusterGroup; + +public class GPSFixMovingWithPolarContext implements PolarClusterKey { + + private final GPSFixMoving fix; + private final TrackedRace race; + private final Competitor competitor; + private final ClusterGroup windSpeedClusterGroup; + private final Set windSourcesToExcludeForBearing; + private final Set windSourcesToExcludeForSpeed; + + public GPSFixMovingWithPolarContext(GPSFixMoving fix, TrackedRace race, Competitor competitor, + ClusterGroup windSpeedClusterGroup) { + this.fix = fix; + this.race = race; + this.competitor = competitor; + this.windSpeedClusterGroup = windSpeedClusterGroup; + this.windSourcesToExcludeForBearing = collectWindSourcesToIgnoreForBearing(); + this.windSourcesToExcludeForSpeed = collectWindSourcesToIgnoreForSpeed(); + } + + public GPSFixMoving getFix() { + return fix; + } + + public TrackedRace getRace() { + return race; + } + + public Competitor getCompetitor() { + return competitor; + } + + @Override + public RoundedAngleToTheWind getRoundedAngleToTheWind() { + final BearingWithConfidence angleToTheWind = getAngleToTheWind(); + return angleToTheWind == null ? null : new RoundedAngleToTheWind(angleToTheWind.getObject()); + } + + public BearingWithConfidence getAngleToTheWind() { + SpeedWithBearing boatSpeed = race.getTrack(competitor).getEstimatedSpeed(fix.getTimePoint()); + WindWithConfidence> wind = race.getWindWithConfidence(fix.getPosition(), fix.getTimePoint(), windSourcesToExcludeForBearing); + + BearingWithConfidenceImpl result = null; + if (wind != null && boatSpeed != null) { + Bearing bearing = boatSpeed.getBearing(); + Bearing difference = wind.getObject().getFrom().getDifferenceTo(bearing); + result = new BearingWithConfidenceImpl(difference, wind.getConfidence(), 0); + } + return result; + } + + @Override + public Cluster getWindSpeedCluster() { + final WindWithConfidence> windWithConfidence = getWindSpeed(); + return windWithConfidence == null ? null : windSpeedClusterGroup.getClusterFor(windWithConfidence.getObject()); + } + + public WindWithConfidence> getWindSpeed() { + WindWithConfidence> wind = race.getWindWithConfidence(fix.getPosition(), fix.getTimePoint(), windSourcesToExcludeForSpeed); + return wind; + } + + public SpeedWithBearingWithConfidence getBoatSpeed() { + GPSFixTrack track = race.getTrack(competitor); + return track.getEstimatedSpeed(fix.getTimePoint(), ConfidenceFactory.INSTANCE.createExponentialTimeDifferenceWeigher( + // use a minimum confidence to avoid the bearing to flip to 270deg in case all is zero + track.getMillisecondsOverWhichToAverageSpeed()/2, /* minimumConfidence */ 0.00000001)); + } + + @Override + public BoatClass getBoatClass() { + return race.getRace().getBoatClass(); + } + + private Set collectWindSourcesToIgnoreForBearing() { + Set windSourcesToExclude = new HashSet(); + Iterable combinedSources = race.getWindSources(WindSourceType.COMBINED); + for (WindSource combinedSource : combinedSources) { + windSourcesToExclude.add(combinedSource); + } + Iterable courseSources = race.getWindSources(WindSourceType.COURSE_BASED); + for (WindSource courseSource : courseSources) { + windSourcesToExclude.add(courseSource); + } + + Iterable expSources = race.getWindSources(WindSourceType.EXPEDITION); + for (WindSource expSource : expSources) { + windSourcesToExclude.add(expSource); + } + Iterable rcSources = race.getWindSources(WindSourceType.RACECOMMITTEE); + for (WindSource rcSource : rcSources) { + windSourcesToExclude.add(rcSource); + } + Iterable webSources = race.getWindSources(WindSourceType.WEB); + for (WindSource webSource : webSources) { + windSourcesToExclude.add(webSource); + } + return windSourcesToExclude; + } + + private Set collectWindSourcesToIgnoreForSpeed() { + Set windSourcesToExclude = new HashSet(); + Iterable combinedSources = race.getWindSources(WindSourceType.COMBINED); + for (WindSource combinedSource : combinedSources) { + windSourcesToExclude.add(combinedSource); + } + Iterable courseSources = race.getWindSources(WindSourceType.COURSE_BASED); + for (WindSource courseSource : courseSources) { + windSourcesToExclude.add(courseSource); + } + Iterable trackBasedSources = race.getWindSources(WindSourceType.TRACK_BASED_ESTIMATION); + for (WindSource trackBasedSource : trackBasedSources) { + windSourcesToExclude.add(trackBasedSource); + } + Iterable rcSources = race.getWindSources(WindSourceType.RACECOMMITTEE); + for (WindSource rcSource : rcSources) { + windSourcesToExclude.add(rcSource); + } + Iterable webSources = race.getWindSources(WindSourceType.WEB); + for (WindSource webSource : webSources) { + windSourcesToExclude.add(webSource); + } + return windSourcesToExclude; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/IncrementalRegressionProcessor.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/IncrementalRegressionProcessor.java new file mode 100644 index 00000000000..b6945159546 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/IncrementalRegressionProcessor.java @@ -0,0 +1,158 @@ +package com.sap.sailing.polars.mining; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.confidence.BearingWithConfidence; +import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +import com.sap.sailing.domain.tracking.WindWithConfidence; +import com.sap.sailing.polars.regression.BoatSpeedEstimator; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.datamining.AdditionalResultDataBuilder; +import com.sap.sse.datamining.components.Processor; +import com.sap.sse.datamining.data.Cluster; +import com.sap.sse.datamining.data.ClusterGroup; +import com.sap.sse.datamining.factories.GroupKeyFactory; +import com.sap.sse.datamining.impl.components.GroupedDataEntry; +import com.sap.sse.datamining.shared.GroupKey; + +public class IncrementalRegressionProcessor implements Processor,Void> { + + private static final Logger logger = Logger.getLogger(IncrementalRegressionProcessor.class.getName()); + + private final Map boatSpeedEstimators = new HashMap(); + + private final ClusterGroup speedClusterGroup; + + private final Set availableBoatClasses = new HashSet<>(); + + public IncrementalRegressionProcessor(ClusterGroup speedClusterGroup) { + this.speedClusterGroup = speedClusterGroup; + } + + @Override + public void processElement(GroupedDataEntry element) { + GroupKey key = element.getKey(); + BoatSpeedEstimator boatSpeedEstimator; + synchronized (boatSpeedEstimators) { + if (!boatSpeedEstimators.containsKey(key)) { + boatSpeedEstimators.put(key, new BoatSpeedEstimator()); + availableBoatClasses.add(element.getDataEntry().getBoatClass()); + } + + boatSpeedEstimator = boatSpeedEstimators.get(key); + } + GPSFixMovingWithPolarContext fix = element.getDataEntry(); + + BearingWithConfidence angleToTheWind = fix.getAngleToTheWind(); + + WindWithConfidence> windSpeed = fix.getWindSpeed(); + + SpeedWithBearingWithConfidence boatSpeedWithConfidence = fix.getBoatSpeed(); + + + //Only add GPS data if speeds and angles are not null, else do nothing! + if (angleToTheWind != null && windSpeed != null && boatSpeedWithConfidence != null) { + + WindWithConfidence> windWithConfidenceForSpeed = windSpeed; + + double confidenceForWindSpeed = windWithConfidenceForSpeed.getConfidence(); + double confidenceForWindBearing = angleToTheWind.getConfidence(); + double confidenceForBoatSpeed = boatSpeedWithConfidence.getConfidence(); + double averagedConfidence = (confidenceForBoatSpeed + confidenceForWindBearing + confidenceForWindSpeed) / 3; + boatSpeedEstimator.addData(windWithConfidenceForSpeed.getObject().getKnots(), angleToTheWind.getObject() + .getDegrees(), boatSpeedWithConfidence.getObject().getKnots(), averagedConfidence); + } + } + + @Override + public void finish() throws InterruptedException { + // Nothing to do here + } + + @Override + public void abort() { + // TODO Auto-generated method stub + } + + @Override + public AdditionalResultDataBuilder getAdditionalResultData(AdditionalResultDataBuilder additionalDataBuilder) { + // TODO Auto-generated method stub + return null; + } + + public Pair, Integer> estimateBoatSpeed(final BoatClass boatClass, final Speed windSpeed, + final Bearing angleToTheWind, boolean useLinearRegression) + throws NotEnoughDataHasBeenAddedException { + int dataCount = 0; + PolarClusterKey key = new PolarClusterKey() { + @Override + public RoundedAngleToTheWind getRoundedAngleToTheWind() { + return new RoundedAngleToTheWind(angleToTheWind); + } + + @Override + public BoatClass getBoatClass() { + return boatClass; + } + + @Override + public Cluster getWindSpeedCluster() { + return speedClusterGroup.getClusterFor(windSpeed); + } + }; + GroupKey compoundKey; + try { + compoundKey = GroupKeyFactory.createCompoundKeyFor(key, PolarDataDimensionCollectionFactory + .getClusterKeyDimensions().iterator()); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + BoatSpeedEstimator boatSpeedEstimator = boatSpeedEstimators.get(compoundKey); + if (boatSpeedEstimator == null) { + throw new NotEnoughDataHasBeenAddedException(); + } + KnotSpeedImpl speedWithoutConfidence = new KnotSpeedImpl(boatSpeedEstimator.estimateSpeed(windSpeed.getKnots(), + angleToTheWind.getDegrees(), useLinearRegression)); + dataCount = boatSpeedEstimator.getDataCount(); + double confidence = boatSpeedEstimator.getConfidence(); + return new Pair, Integer>(new SpeedWithConfidenceImpl( + speedWithoutConfidence, confidence, null), dataCount); + } + + @Override + public void onFailure(Throwable failure) { + logger.severe("Polar Data Mining Pipe failed."); + throw new RuntimeException("Polar Data Miner failed.", failure); + } + + public Set getAvailableBoatClasses() { + return availableBoatClasses; + } + + @Override + public Class> getInputType() { + // TODO Auto-generated method stub + return null; + } + + @Override + public Class getResultType() { + // No result type here, since this is a special case of a processor. It's the end of the pipe so to say. + return null; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarClusterKey.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarClusterKey.java new file mode 100644 index 00000000000..51e89b3298d --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarClusterKey.java @@ -0,0 +1,27 @@ +package com.sap.sailing.polars.mining; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.common.Speed; +import com.sap.sse.datamining.data.Cluster; +import com.sap.sse.datamining.shared.annotations.Dimension; + +public interface PolarClusterKey { + + /** + * + * @return degrees boat to windDirection in DEG + */ + @Dimension(messageKey = "angleToWind") + RoundedAngleToTheWind getRoundedAngleToTheWind(); + + /** + * + * @return wind speed cluster + */ + @Dimension(messageKey = "windSpeed") + Cluster getWindSpeedCluster(); + + @Dimension(messageKey = "boatClass") + BoatClass getBoatClass(); + +} \ No newline at end of file diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarDataDimensionCollectionFactory.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarDataDimensionCollectionFactory.java new file mode 100644 index 00000000000..8ded454c339 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarDataDimensionCollectionFactory.java @@ -0,0 +1,29 @@ +package com.sap.sailing.polars.mining; + +import java.util.ArrayList; +import java.util.Collection; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sse.datamining.factories.FunctionFactory; +import com.sap.sse.datamining.functions.Function; + +public class PolarDataDimensionCollectionFactory { + + public static Collection> getClusterKeyDimensions() throws NoSuchMethodException { + Collection> dimensions = new ArrayList<>(); + FunctionFactory functionFactory = new FunctionFactory(); + Function angleFunction = functionFactory + .createMethodWrappingFunction(PolarClusterKey.class.getMethod("getRoundedAngleToTheWind", + new Class[0])); + Function windSpeedFunction = functionFactory.createMethodWrappingFunction(PolarClusterKey.class + .getMethod("getWindSpeedCluster", new Class[0])); + Function boatClassFunction = functionFactory.createMethodWrappingFunction(PolarClusterKey.class + .getMethod("getBoatClass", new Class[0])); + + dimensions.add(boatClassFunction); + dimensions.add(angleFunction); + dimensions.add(windSpeedFunction); + return dimensions; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarDataMiner.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarDataMiner.java new file mode 100644 index 00000000000..b5b422e9b6c --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarDataMiner.java @@ -0,0 +1,230 @@ +package com.sap.sailing.polars.mining; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl; +import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.PolarSheetGenerationSettings; +import com.sap.sailing.domain.common.PolarSheetsData; +import com.sap.sailing.domain.common.PolarSheetsHistogramData; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +import com.sap.sailing.domain.common.impl.PolarSheetGenerationSettingsImpl; +import com.sap.sailing.domain.common.impl.PolarSheetsDataImpl; +import com.sap.sailing.domain.common.impl.PolarSheetsHistogramDataImpl; +import com.sap.sailing.domain.common.impl.WindSteppingWithMaxDistance; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; +import com.sap.sse.common.Util.Pair; +import com.sap.sse.datamining.components.Processor; +import com.sap.sse.datamining.data.ClusterGroup; +import com.sap.sse.datamining.functions.Function; +import com.sap.sse.datamining.impl.components.GroupedDataEntry; +import com.sap.sse.datamining.impl.components.ParallelFilteringProcessor; +import com.sap.sse.datamining.impl.components.ParallelMultiDimensionsValueNestingGroupingProcessor; + +public class PolarDataMiner { + + private static final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3); + private static final ThreadPoolExecutor executor = createExecutor(); + private final PolarSheetGenerationSettings backendPolarSheetGenerationSettings; + + private static ThreadPoolExecutor createExecutor() { + return new ThreadPoolExecutor(THREAD_POOL_SIZE, THREAD_POOL_SIZE, 60, TimeUnit.SECONDS, + new LinkedBlockingQueue()); + } + + + private AbstractEnrichingProcessor enrichingProcessor; + private IncrementalRegressionProcessor incrementalRegressionProcessor; + + public PolarDataMiner() { + backendPolarSheetGenerationSettings = PolarSheetGenerationSettingsImpl.createBackendPolarSettings(); + try { + setUpWorkflow(); + } catch (ClassCastException | NoSuchMethodException | SecurityException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + + + private void setUpWorkflow() throws ClassCastException, NoSuchMethodException, + SecurityException { + WindSteppingWithMaxDistance stepping = backendPolarSheetGenerationSettings.getWindStepping(); + final ClusterGroup speedClusterGroup = SpeedClusterGroupFromWindSteppingCreator + .createSpeedClusterGroupFrom(stepping); + incrementalRegressionProcessor = new IncrementalRegressionProcessor(speedClusterGroup); + Collection, ?>> grouperResultReceivers = new ArrayList, ?>>(); + grouperResultReceivers.add(incrementalRegressionProcessor); + + Collection> dimensions = PolarDataDimensionCollectionFactory.getClusterKeyDimensions(); + + Processor> groupingProcessor = new ParallelMultiDimensionsValueNestingGroupingProcessor( + GPSFixMovingWithPolarContext.class, executor, grouperResultReceivers, dimensions); + + Collection> filteringResultReceivers = Arrays.asList(groupingProcessor); + Processor filteringProcessor = new ParallelFilteringProcessor( + GPSFixMovingWithPolarContext.class, executor, filteringResultReceivers, new PolarFixFilterCriteria()); + + Collection> enrichingResultReceivers = Arrays + .asList(filteringProcessor); + + + enrichingProcessor = new AbstractEnrichingProcessor( + GPSFixMovingWithOriginInfo.class, GPSFixMovingWithPolarContext.class, executor, enrichingResultReceivers) { + + @Override + protected GPSFixMovingWithPolarContext enrich(GPSFixMovingWithOriginInfo element) { + return new GPSFixMovingWithPolarContext(element.getFix(), element.getTrackedRace(), element.getCompetitor(), + speedClusterGroup); + } + }; + } + + + public void addFix(GPSFixMoving fix, Competitor competitor, TrackedRace trackedRace) { + enrichingProcessor.processElement(new GPSFixMovingWithOriginInfo(fix, trackedRace, + competitor)); + } + + public boolean isCurrentlyActiveAndOrHasQueue() { + boolean isActive = executor.getActiveCount() > 0; + boolean hasQueue = executor.getQueue().size() > 0; + return isActive || hasQueue; + } + + /** + * + * @param boatClass + * @param windSpeed + * @param angleToTheWind + * @param useLinearRegression if true uses lin. regression in the wind interval, otherwise arithm. mean + * @return + * @throws NotEnoughDataHasBeenAddedException + */ + public SpeedWithConfidence estimateBoatSpeed(BoatClass boatClass, Speed windSpeed, Bearing angleToTheWind, + boolean useLinearRegression) + throws NotEnoughDataHasBeenAddedException { + return incrementalRegressionProcessor.estimateBoatSpeed(boatClass, windSpeed, angleToTheWind, useLinearRegression).getA(); + } + + + + public PolarSheetsData createFullSheetForBoatClass(BoatClass boatClass) { + Double[] defaultWindSpeeds = backendPolarSheetGenerationSettings.getWindStepping().getRawStepping(); + Number[][] averagedPolarDataByWindSpeed = new Number[defaultWindSpeeds.length][360]; + + Map dataCountPerAngleForWindspeed = new HashMap<>(); + Map> histogramDataMap = new HashMap<>(); + + int totalDataCount = 0; + + for (int windIndex = 0; windIndex < defaultWindSpeeds.length; windIndex++) { + Double windSpeed = defaultWindSpeeds[windIndex]; + + Integer[] perAngle = new Integer[360]; + Map perWindSpeed = new HashMap<>(); + for (int angle = 0; angle < 360; angle++) { + Pair, Integer> speedWithConfidenceAndDataCount; + try { + int convertedAngle = convertAngleIfNecessary(angle); + speedWithConfidenceAndDataCount = incrementalRegressionProcessor.estimateBoatSpeed(boatClass, + new KnotSpeedImpl(windSpeed), new DegreeBearingImpl(convertedAngle), true); + } catch (NotEnoughDataHasBeenAddedException e) { + // No data so put in a 0 speed with 0 confidence + speedWithConfidenceAndDataCount = new Pair, Integer>( + new SpeedWithConfidenceImpl(new KnotSpeedImpl(0), 0, null), 0); + } + + averagedPolarDataByWindSpeed[windIndex][angle] = speedWithConfidenceAndDataCount.getA().getObject().getKnots(); + int dataCount = speedWithConfidenceAndDataCount.getB(); + + totalDataCount = totalDataCount + dataCount; + //FIXME hard coded + double coefficiantOfVariation = 0.8; + double confidenceMeasure = 0.5; + + PolarSheetsHistogramDataImpl polarSheetsHistogramDataImpl = createEmptyHistogramData(perAngle, angle, + dataCount, coefficiantOfVariation, confidenceMeasure); + perWindSpeed.put(angle, polarSheetsHistogramDataImpl); + } + histogramDataMap.put(windIndex, perWindSpeed); + dataCountPerAngleForWindspeed.put(windIndex, perAngle); + } + + + PolarSheetsData data = new PolarSheetsDataImpl(averagedPolarDataByWindSpeed, totalDataCount, + dataCountPerAngleForWindspeed, backendPolarSheetGenerationSettings.getWindStepping(), histogramDataMap); + return data; + } + + + + private int convertAngleIfNecessary(int angle) { + int convertedAngle = angle; + if (angle > 180) { + convertedAngle = angle - 360; + } + return convertedAngle; + } + + + + private PolarSheetsHistogramDataImpl createEmptyHistogramData(Integer[] perAngle, int angle, int dataCount, + double coefficiantOfVariation, double confidenceMeasure) { + perAngle[angle] = dataCount; + Number[] xValues = {}; + Number[] yValues = {};; + Map yValuesByGaugeIds = new HashMap<>(); + Map yValuesByDay = new HashMap<>(); + Map yValuesByDayAndGaugeId = new HashMap<>(); + + + PolarSheetsHistogramDataImpl polarSheetsHistogramDataImpl = new PolarSheetsHistogramDataImpl(angle, xValues, yValues, yValuesByGaugeIds, + yValuesByDay, yValuesByDayAndGaugeId, dataCount, coefficiantOfVariation); + + polarSheetsHistogramDataImpl.setConfidenceMeasure(confidenceMeasure); + return polarSheetsHistogramDataImpl; + } + + + + public Set getAvailableBoatClasses() { + return incrementalRegressionProcessor.getAvailableBoatClasses(); + } + + + + public Integer[] getDataCountsForWindSpeed(BoatClass boatClass, Speed windSpeed, int startAngleInclusive, int endAngleExclusive) { + Integer[] dataCounts = new Integer[360]; + for (int angle = 0; angle < 360; angle++) { + if (angle >= startAngleInclusive && angle < endAngleExclusive) { + try { + dataCounts[angle] = incrementalRegressionProcessor.estimateBoatSpeed(boatClass, windSpeed, + new DegreeBearingImpl(convertAngleIfNecessary(angle)), true).getB(); + } catch (NotEnoughDataHasBeenAddedException e) { + dataCounts[angle] = 0; + } + } else { + dataCounts[angle] = null; + } + } + return dataCounts; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarFixFilterCriteria.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarFixFilterCriteria.java new file mode 100644 index 00000000000..a6d5315122d --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/PolarFixFilterCriteria.java @@ -0,0 +1,90 @@ +package com.sap.sailing.polars.mining; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.Course; +import com.sap.sailing.domain.tracking.GPSFixMoving; +import com.sap.sailing.domain.tracking.GPSFixTrack; +import com.sap.sailing.domain.tracking.MarkPassing; +import com.sap.sailing.domain.tracking.TrackedRace; +import com.sap.sse.common.TimePoint; +import com.sap.sse.datamining.components.FilterCriterion; + +public class PolarFixFilterCriteria implements FilterCriterion { + + @Override + public boolean matches(GPSFixMovingWithPolarContext element) { + boolean afterStartTime = isAfterStartTime(element); + boolean beforeFinishTime = isBeforeFinishTime(element); + boolean noDirectionChange = !hasDirectionChange(element); + return (afterStartTime && beforeFinishTime && noDirectionChange); + } + + private boolean hasDirectionChange(GPSFixMovingWithPolarContext element) { + GPSFixTrack track = element.getRace().getTrack(element.getCompetitor()); + return track.hasDirectionChange(element.getFix().getTimePoint(), element.getRace().getRace().getBoatClass() + .getManeuverDegreeAngleThreshold()); + } + + private boolean isBeforeFinishTime(GPSFixMovingWithPolarContext element) { + TimePoint timepointOfFix = element.getFix().getTimePoint(); + boolean isBeforeFinish; + TimePoint finishTime = calculateFinishTime(element.getRace(), element.getCompetitor()); + isBeforeFinish = finishTime == null || timepointOfFix.before(finishTime); + return isBeforeFinish; + } + + private TimePoint calculateFinishTime(TrackedRace race, Competitor competitor) { + TimePoint finishTime = race.getEndOfRace(); + if (finishTime != null) { + Course course = race.getRace().getCourse(); + if (course.getLastWaypoint() != course.getFirstWaypoint()) { + MarkPassing finishPassing = race.getMarkPassing(competitor, course.getLastWaypoint()); + if (finishPassing != null) { + TimePoint passedFinishTimePoint = finishPassing.getTimePoint(); + if (passedFinishTimePoint.before(finishTime)) { + finishTime = passedFinishTimePoint; + } + } + } + } + return finishTime; + } + + private boolean isAfterStartTime(GPSFixMovingWithPolarContext element) { + TimePoint timepointOfFix = element.getFix().getTimePoint(); + boolean isAfterStart; + try { + TimePoint startTime = calculateStartTime(element.getRace(), element.getCompetitor()); + isAfterStart = timepointOfFix.after(startTime); + } catch (CompetitorDidNotStartYetException exception) { + isAfterStart = false; + } + return isAfterStart; + } + + private TimePoint calculateStartTime(TrackedRace race, Competitor competitor) throws CompetitorDidNotStartYetException { + MarkPassing startPassing = race.getMarkPassing(competitor, race.getRace().getCourse().getFirstWaypoint()); + if (startPassing == null) { + throw new CompetitorDidNotStartYetException(); + } + TimePoint raceStartTime = race.getStartOfRace(); + TimePoint startTime; + TimePoint passedStartTimePoint = startPassing.getTimePoint(); + if (passedStartTimePoint.after(raceStartTime)) { + startTime = passedStartTimePoint; + } else { + startTime = raceStartTime; + } + return startTime; + } + + private class CompetitorDidNotStartYetException extends Exception { + private static final long serialVersionUID = 7906688735433666009L; + } + + @Override + public Class getElementType() { + return GPSFixMovingWithPolarContext.class; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/RoundedAngleToTheWind.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/RoundedAngleToTheWind.java new file mode 100644 index 00000000000..b0d48bdf912 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/RoundedAngleToTheWind.java @@ -0,0 +1,40 @@ +package com.sap.sailing.polars.mining; + +import com.sap.sailing.domain.common.Bearing; + +public class RoundedAngleToTheWind { + + private final int angle; + + public RoundedAngleToTheWind(Bearing angleToTheWind) { + this.angle = (int) Math.round(angleToTheWind.getDegrees()); + } + + @Override + public String toString() { + return "" + angle; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + angle; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RoundedAngleToTheWind other = (RoundedAngleToTheWind) obj; + if (angle != other.angle) + return false; + return true; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/SpeedClusterGroupFromWindSteppingCreator.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/SpeedClusterGroupFromWindSteppingCreator.java new file mode 100644 index 00000000000..8b6262d4231 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/SpeedClusterGroupFromWindSteppingCreator.java @@ -0,0 +1,21 @@ +package com.sap.sailing.polars.mining; + +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.impl.WindSteppingWithMaxDistance; +import com.sap.sailing.polars.clusters.SpeedClusterGroup; +import com.sap.sse.datamining.data.ClusterGroup; + +public class SpeedClusterGroupFromWindSteppingCreator { + + public static ClusterGroup createSpeedClusterGroupFrom(WindSteppingWithMaxDistance windStepping) { + double maxDistance = windStepping.getMaxDistance(); + Double[] rawIntegerStepping = windStepping.getRawStepping(); + double[] rawDoubleLevelMids = new double[rawIntegerStepping.length]; + for (int i = 0; i < rawIntegerStepping.length; i++) { + rawDoubleLevelMids[i] = rawIntegerStepping[i]; + } + + return new SpeedClusterGroup("Speed Cluster Group", rawDoubleLevelMids, maxDistance); + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/WindSpeedLevel.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/WindSpeedLevel.java new file mode 100644 index 00000000000..08857cf7d12 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/mining/WindSpeedLevel.java @@ -0,0 +1,52 @@ +package com.sap.sailing.polars.mining; + +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.WindStepping; + +public class WindSpeedLevel { + + private final int level; + private final WindStepping stepping; + + public WindSpeedLevel(Speed windSpeed, WindStepping stepping) { + this.level = stepping.getLevelIndexForValue(windSpeed.getKnots()); + this.stepping = stepping; + } + + @Override + public String toString() { + // FIXME right now the toString equals for different steppings. This will resolve in an equal groupkey in the + // datamining framework. The current approach is to use only the default WindStepping. + return "" + level; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + level; + result = prime * result + ((stepping == null) ? 0 : stepping.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + WindSpeedLevel other = (WindSpeedLevel) obj; + if (level != other.level) + return false; + if (stepping == null) { + if (other.stepping != null) + return false; + } else if (!stepping.equals(other.stepping)) + return false; + return true; + } + + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/BoatSpeedEstimator.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/BoatSpeedEstimator.java new file mode 100644 index 00000000000..2d175578be3 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/BoatSpeedEstimator.java @@ -0,0 +1,80 @@ +package com.sap.sailing.polars.regression; + +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import com.sap.sailing.polars.mining.IncrementalRegressionProcessor; +import com.sap.sailing.polars.regression.impl.IncrementalLeastSquaresProcessor; + +/** + * Combines two linear regression processors. One with the angle to the wind as the x axis and the other with the wind + * speed on the x axis. Both y axis represent the boat speed. + * + * {@link #estimateSpeed(double, double)} returns the average of the estimation of both regressions. + * + * Note that this class should only be used for a small interval on a polar sheet. The + * {@link IncrementalRegressionProcessor} is one example for that. It has one instance of the {@link BoatSpeedEstimator} + * for each wind speed level and rounded angle combination. + * + * @author Frederik Petersen (D054528) + * + */ +public class BoatSpeedEstimator { + + private IncrementalLinearRegressionProcessor angleRegression = new IncrementalLeastSquaresProcessor(); + + private IncrementalLinearRegressionProcessor windSpeedRegression = new IncrementalLeastSquaresProcessor(); + + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + + private double confidenceSum = 0; + + private int dataCount = 0; + + /** + * + * @param windSpeed + * @param angleToTheWind + * @param useLinearRegression if false mean of wind interval is used, otherwise lin. regression + * @return + * @throws NotEnoughDataHasBeenAddedException + */ + public double estimateSpeed(double windSpeed, double angleToTheWind, boolean useLinearRegression) throws NotEnoughDataHasBeenAddedException { + lock.readLock().lock(); + double result; + try { + if (useLinearRegression) { + double angleEstimated = angleRegression.getEstimatedY(angleToTheWind); + double windSpeedEstimated = windSpeedRegression.getEstimatedY(windSpeed); + + result = (angleEstimated + windSpeedEstimated) / 2.0; + } else { + //Return average for that wind interval + result = windSpeedRegression.getMeanOfY(); + } + } finally { + lock.readLock().unlock(); + } + return result; + } + + public void addData(double windSpeed, double angleToTheWind, double boatSpeed, double confidence) { + lock.writeLock().lock(); + try { + angleRegression.addMeasuredPoint(angleToTheWind, boatSpeed); + windSpeedRegression.addMeasuredPoint(windSpeed, boatSpeed); + dataCount++; + confidenceSum = confidenceSum + confidence; + } finally { + lock.writeLock().unlock(); + } + } + + public int getDataCount() { + return dataCount; + } + + public double getConfidence() { + return confidenceSum / dataCount; + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/IncrementalLinearRegressionProcessor.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/IncrementalLinearRegressionProcessor.java new file mode 100644 index 00000000000..d4babfb0573 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/IncrementalLinearRegressionProcessor.java @@ -0,0 +1,17 @@ +package com.sap.sailing.polars.regression; + +public interface IncrementalLinearRegressionProcessor { + + double getEstimatedY(double x) throws NotEnoughDataHasBeenAddedException; + + void addMeasuredPoint(double x, double y); + + double getSlope(); + + double getIntercept(); + + int getDataCount(); + + double getMeanOfY() throws NotEnoughDataHasBeenAddedException; + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/NotEnoughDataHasBeenAddedException.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/NotEnoughDataHasBeenAddedException.java new file mode 100644 index 00000000000..d15ad5ac066 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/NotEnoughDataHasBeenAddedException.java @@ -0,0 +1,15 @@ +package com.sap.sailing.polars.regression; + +public class NotEnoughDataHasBeenAddedException extends Exception { + + private static final long serialVersionUID = 4134116458606258315L; + + public NotEnoughDataHasBeenAddedException() { + super("No data has been added to the Processor yet. At least to value-pairs have to be added."); + } + + public NotEnoughDataHasBeenAddedException(String string) { + super(string); + } + +} diff --git a/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/impl/IncrementalLeastSquaresProcessor.java b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/impl/IncrementalLeastSquaresProcessor.java new file mode 100644 index 00000000000..64b99193c44 --- /dev/null +++ b/java/com.sap.sailing.polars/src/com/sap/sailing/polars/regression/impl/IncrementalLeastSquaresProcessor.java @@ -0,0 +1,130 @@ +package com.sap.sailing.polars.regression.impl; + +import com.sap.sailing.polars.regression.IncrementalLinearRegressionProcessor; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; + +/** + * Allows a linear regression of a 2 dimensional and growing dataset. It has a complexity of O(1) + * + * The algorithm and variable naming is similar to the german wikipedia + * article + * + * The difference is that some parts of the equation are memorized as fields and updated incrementally. + * + * @author Frederik Petersen (D054528) + * + */ +public class IncrementalLeastSquaresProcessor implements IncrementalLinearRegressionProcessor { + + private double alpha1 = 0; + + private double alpha0 = 0; + + private double partA = 0; + + private double partB = 0; + + private double meanOfX = 0; + + private double meanOfY = 0; + + private int numberOfPointsAdded = 0; + + @Override + public double getEstimatedY(double x) throws NotEnoughDataHasBeenAddedException { + if (numberOfPointsAdded < 1) { + throw new NotEnoughDataHasBeenAddedException(); + } + + double result; + if (numberOfPointsAdded < 2) { + // Return the y value of the only added data point. + result = meanOfY; + } else { + result = alpha0 + alpha1 * x; + } + + return result; + } + + @Override + public void addMeasuredPoint(double x, double y) { + numberOfPointsAdded++; + + calculatePartA(x, y); + calculatePartB(x, y); + + addToArithmeticMeanOfX(x); + addToArithmeticMeanOfY(y); + + calculateFactor1(); + calculateFactor2(); + } + + private void calculateFactor2() { + alpha0 = meanOfY - alpha1 * meanOfX; + } + + private void calculateFactor1() { + double numerator = calculateNumeratorOfFactor1(); + double denominator = calculateDenominatorOfFactor1(); + if (denominator == 0) { + alpha1 = 0; + } else { + alpha1 = numerator / denominator; + } + } + + private double calculateNumeratorOfFactor1() { + return partA - numberOfPointsAdded * meanOfX * meanOfY; + } + + private double calculateDenominatorOfFactor1() { + return partB - numberOfPointsAdded * meanOfX * meanOfX; + } + + private void addToArithmeticMeanOfY(double y) { + meanOfY = addToMean(y, meanOfY); + } + + private void addToArithmeticMeanOfX(double x) { + meanOfX = addToMean(x, meanOfX); + } + + private double addToMean(double z, double oldMean) { + return oldMean + (z - oldMean) / numberOfPointsAdded; + } + + private void calculatePartB(double x, double y) { + partB = partB + x * x; + } + + private void calculatePartA(double x, double y) { + partA = partA + x * y; + } + + @Override + public double getSlope() { + return alpha1; + } + + @Override + public double getIntercept() { + return alpha0; + } + + @Override + public int getDataCount() { + return numberOfPointsAdded; + } + + @Override + public double getMeanOfY() throws NotEnoughDataHasBeenAddedException { + if (numberOfPointsAdded < 1) { + throw new NotEnoughDataHasBeenAddedException(); + } + return meanOfY; + } + +} diff --git a/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/deserialization/impl/SpeedWithConfidenceWithIntegerRelationJsonDeserializer.java b/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/deserialization/impl/SpeedWithConfidenceWithIntegerRelationJsonDeserializer.java new file mode 100644 index 00000000000..cf9cf344254 --- /dev/null +++ b/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/deserialization/impl/SpeedWithConfidenceWithIntegerRelationJsonDeserializer.java @@ -0,0 +1,26 @@ +package com.sap.sailing.server.gateway.deserialization.impl; + +import org.json.simple.JSONObject; + +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.domain.base.impl.SpeedWithConfidenceImpl; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializationException; +import com.sap.sailing.server.gateway.deserialization.JsonDeserializer; +import com.sap.sailing.server.gateway.serialization.impl.SpeedWithConfidenceWithIntegerRelationJsonSerializer; + +public class SpeedWithConfidenceWithIntegerRelationJsonDeserializer implements + JsonDeserializer> { + + @Override + public SpeedWithConfidence deserialize(JSONObject object) throws JsonDeserializationException { + double speedInKnots = (Double) object.get(SpeedWithConfidenceWithIntegerRelationJsonSerializer.FIELD_SPEED); + double confidence = (Double) object.get(SpeedWithConfidenceWithIntegerRelationJsonSerializer.FIELD_CONFIDENCE); + int relativeToValue = (Integer) object + .get(SpeedWithConfidenceWithIntegerRelationJsonSerializer.FIELD_RELATION_INT); + Speed speed = new KnotSpeedImpl(speedInKnots); + return new SpeedWithConfidenceImpl(speed, confidence, relativeToValue); + } + +} diff --git a/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/SpeedWithConfidenceWithIntegerRelationJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/SpeedWithConfidenceWithIntegerRelationJsonSerializer.java new file mode 100644 index 00000000000..887934e3450 --- /dev/null +++ b/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/SpeedWithConfidenceWithIntegerRelationJsonSerializer.java @@ -0,0 +1,24 @@ +package com.sap.sailing.server.gateway.serialization.impl; + +import org.json.simple.JSONObject; + +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.server.gateway.serialization.JsonSerializer; + +public class SpeedWithConfidenceWithIntegerRelationJsonSerializer implements + JsonSerializer> { + + public static final String FIELD_SPEED = "speed"; + public static final String FIELD_CONFIDENCE = "confidence"; + public static final String FIELD_RELATION_INT = "relationInt"; + + @Override + public JSONObject serialize(SpeedWithConfidence speed) { + JSONObject object = new JSONObject(); + object.put(FIELD_SPEED, speed.getObject().getKnots()); + object.put(FIELD_CONFIDENCE, speed.getConfidence()); + object.put(FIELD_RELATION_INT, speed.getRelativeTo()); + return object; + } + +} diff --git a/java/com.sap.sailing.server.gateway/META-INF/MANIFEST.MF b/java/com.sap.sailing.server.gateway/META-INF/MANIFEST.MF index 8f88fdb1083..82b306d061d 100644 --- a/java/com.sap.sailing.server.gateway/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.server.gateway/META-INF/MANIFEST.MF @@ -1,50 +1,52 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Sailing Server Gateway -Bundle-SymbolicName: com.sap.sailing.server.gateway -Bundle-Version: 1.0.0.qualifier -Bundle-Vendor: SAP -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Import-Package: com.sap.sailing.server, - com.sap.sailing.server.operationaltransformation, - com.sap.sailing.server.racelog.tracking, - javax.ws.rs;version="1.1.1", - javax.ws.rs.core;version="1.1.1", - javax.ws.rs.ext;version="1.1.1", - org.apache.commons.lang, - org.json.simple, - org.json.simple.parser, - org.osgi.framework;version="1.3.0", - org.osgi.util.tracker;version="1.3.1" -Require-Bundle: com.sap.sailing.domain, - com.sap.sailing.expeditionconnector, - com.sap.sailing.declination, - com.sap.sailing.domain.tractracadapter, - com.sap.sailing.domain.persistence, - com.sap.sse.mongodb, - com.mongodb.driver;bundle-version="2.6.2", - javax.servlet;bundle-version="2.6.0", - com.sap.sailing.domain.common, - com.sap.sailing.udpconnector, - org.jdom;bundle-version="1.0.0", - com.sap.sailing.server.gateway.serialization;bundle-version="1.0.0", - com.sap.sailing.domain.shared.android;bundle-version="1.0.0", - com.sap.sailing.server.trackfiles;bundle-version="1.0.0", - org.eclipse.jetty.websocket.api;bundle-version="9.0.4", - org.eclipse.jetty.websocket.server;bundle-version="9.0.4", - org.eclipse.jetty.websocket.servlet;bundle-version="9.0.4", - org.eclipse.jetty.websocket.client;bundle-version="9.0.4", - org.eclipse.jetty.websocket.common;bundle-version="9.0.4", - javax.validation;bundle-version="1.0.0", - com.sun.jersey;bundle-version="1.17.0", - org.objectweb.asm;bundle-version="3.3.1", - org.apache.commons.fileupload;bundle-version="1.2.2", - com.sap.sailing.domain.igtimiadapter;bundle-version="1.0.0", - com.sap.sse.common, - com.sap.sse.replication, - com.sap.sse -Bundle-ClassPath: . -Web-ContextPath: /sailingserver -Export-Package: com.sap.sailing.server.gateway, - com.sap.sailing.server.gateway.jaxrs, - com.sap.sailing.server.gateway.jaxrs.spi +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Sailing Server Gateway +Bundle-SymbolicName: com.sap.sailing.server.gateway +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Import-Package: com.sap.sailing.polars, + com.sap.sailing.polars.regression, + com.sap.sailing.server, + com.sap.sailing.server.operationaltransformation, + com.sap.sailing.server.racelog.tracking, + javax.ws.rs;version="1.1.1", + javax.ws.rs.core;version="1.1.1", + javax.ws.rs.ext;version="1.1.1", + org.apache.commons.lang, + org.json.simple, + org.json.simple.parser, + org.osgi.framework;version="1.3.0", + org.osgi.util.tracker;version="1.3.1" +Require-Bundle: com.sap.sailing.domain, + com.sap.sailing.expeditionconnector, + com.sap.sailing.declination, + com.sap.sailing.domain.tractracadapter, + com.sap.sailing.domain.persistence, + com.sap.sse.mongodb, + com.mongodb.driver;bundle-version="2.6.2", + javax.servlet;bundle-version="2.6.0", + com.sap.sailing.domain.common, + com.sap.sailing.udpconnector, + org.jdom;bundle-version="1.0.0", + com.sap.sailing.server.gateway.serialization;bundle-version="1.0.0", + com.sap.sailing.domain.shared.android;bundle-version="1.0.0", + com.sap.sailing.server.trackfiles;bundle-version="1.0.0", + org.eclipse.jetty.websocket.api;bundle-version="9.0.4", + org.eclipse.jetty.websocket.server;bundle-version="9.0.4", + org.eclipse.jetty.websocket.servlet;bundle-version="9.0.4", + org.eclipse.jetty.websocket.client;bundle-version="9.0.4", + org.eclipse.jetty.websocket.common;bundle-version="9.0.4", + javax.validation;bundle-version="1.0.0", + com.sun.jersey;bundle-version="1.17.0", + org.objectweb.asm;bundle-version="3.3.1", + org.apache.commons.fileupload;bundle-version="1.2.2", + com.sap.sailing.domain.igtimiadapter;bundle-version="1.0.0", + com.sap.sse.common, + com.sap.sse.replication, + com.sap.sse +Bundle-ClassPath: . +Web-ContextPath: /sailingserver +Export-Package: com.sap.sailing.server.gateway, + com.sap.sailing.server.gateway.jaxrs, + com.sap.sailing.server.gateway.jaxrs.spi diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/PolarResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/PolarResource.java new file mode 100644 index 00000000000..b610876cca5 --- /dev/null +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/PolarResource.java @@ -0,0 +1,80 @@ +package com.sap.sailing.server.gateway.jaxrs.api; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; + +import com.sap.sailing.domain.base.BoatClass; +import com.sap.sailing.domain.base.SpeedWithBearingWithConfidence; +import com.sap.sailing.domain.base.SpeedWithConfidence; +import com.sap.sailing.domain.common.LegType; +import com.sap.sailing.domain.common.Speed; +import com.sap.sailing.domain.common.Tack; +import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.domain.common.impl.KnotSpeedImpl; +import com.sap.sailing.polars.PolarDataService; +import com.sap.sailing.polars.regression.NotEnoughDataHasBeenAddedException; +import com.sap.sailing.server.gateway.jaxrs.AbstractSailingServerResource; + +/** + * Right now this service is only used for quick debugging and testing of the polar api. It used plain text responses. + * JSON should be used if this resource is to be consumed. + * + * @author Frederik Petersen + * + */ +@Path("/v1/polars") +public class PolarResource extends AbstractSailingServerResource { + + @GET + @Produces("text/plain;charset=UTF-8") + @Path("{boatClassName}") + public Response getSpeed(@PathParam("boatClassName") String boatClassName, @QueryParam("angle") double angle, + @QueryParam("windspeed") double windSpeed) { + BoatClass boatClass = getService().getDomainObjectFactory().getBaseDomainFactory() + .getOrCreateBoatClass(boatClassName); + SpeedWithConfidence speedWithConfidence; + ResponseBuilder responseBuilder; + try { + speedWithConfidence = getService().getPolarDataService().getSpeed(boatClass, new KnotSpeedImpl(windSpeed), + new DegreeBearingImpl(angle), true); + String resultString = "Speed: " + speedWithConfidence.getObject().getKnots() + "kn; Confidence: " + + speedWithConfidence.getConfidence(); + responseBuilder = Response.ok(resultString, MediaType.TEXT_PLAIN); + } catch (NotEnoughDataHasBeenAddedException e) { + responseBuilder = Response.noContent(); + } + + return responseBuilder.build(); + } + + @GET + @Produces("text/plain;charset=UTF-8") + @Path("average/{boatClassName}") + public Response getAverageSpeedAndBearing(@PathParam("boatClassName") String boatClassName, + @QueryParam("windspeed") double wSpeed, @QueryParam("legtype") LegType legType, @QueryParam("tack") Tack tack) { + BoatClass boatClass = getService().getDomainObjectFactory().getBaseDomainFactory() + .getOrCreateBoatClass(boatClassName); + ResponseBuilder responseBuilder; + Speed windSpeed = new KnotSpeedImpl(wSpeed); + try { + PolarDataService service = getService().getPolarDataService(); + SpeedWithBearingWithConfidence speedWithBearing = service.getAverageSpeedWithBearing(boatClass, + windSpeed, legType, tack); + String resultString = "Speed: " + speedWithBearing.getObject().getKnots() + "kn; Angle: " + + speedWithBearing.getObject().getBearing().getDegrees() + "°; Confidence: " + + speedWithBearing.getConfidence(); + responseBuilder = Response.ok(resultString, MediaType.TEXT_PLAIN); + } catch (NotEnoughDataHasBeenAddedException e) { + responseBuilder = Response.noContent(); + } + + return responseBuilder.build(); + } + +} diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RestApiApplication.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RestApiApplication.java index c779f0be9c5..7b914fa26b4 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RestApiApplication.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RestApiApplication.java @@ -13,6 +13,7 @@ public class RestApiApplication extends Application { classes.add(EventsResource.class); classes.add(RegattasResource.class); classes.add(LeaderboardsResource.class); + classes.add(PolarResource.class); classes.add(SearchResource.class); classes.add(GPSFixesResource.class); return classes; diff --git a/java/com.sap.sailing.server.replication.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.server.replication.test/META-INF/MANIFEST.MF old mode 100644 new mode 100755 index 611902b0541..e62721228be --- a/java/com.sap.sailing.server.replication.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.server.replication.test/META-INF/MANIFEST.MF @@ -1,27 +1,28 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Server Replication Test -Bundle-SymbolicName: com.sap.sailing.server.replication.test -Bundle-Version: 1.0.0.qualifier -Bundle-Vendor: SAP -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Fragment-Host: com.sap.sse.replication -Import-Package: com.rabbitmq.client;version="2.8.4" -Require-Bundle: com.sap.sailing.server, - com.sap.sailing.domain.common, - com.sap.sailing.domain, - com.sap.sailing.domain.test, - com.sap.sse.mongodb, - com.mongodb.driver;bundle-version="2.6.2", - com.sap.sailing.domain.persistence, - com.sap.sailing.domain.tractracadapter, - com.tractrac.clientmodule;bundle-version="2.0.0", - com.sap.sailing.expeditionconnector, - com.sap.sailing.domain.swisstimingadapter, - org.json.simple;bundle-version="1.1.0", - org.junit4;bundle-version="4.8.2", - org.hamcrest;bundle-version="1.1.0", - com.sap.sse.common, - org.mockito.mockito-core;bundle-version="1.9.5", - com.sap.sailing.domain.shared.android, - com.sap.sse.replication.testsupport +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Server Replication Test +Bundle-SymbolicName: com.sap.sailing.server.replication.test +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Fragment-Host: com.sap.sse.replication +Import-Package: com.rabbitmq.client;version="2.8.4" +Require-Bundle: com.sap.sailing.server, + com.sap.sailing.domain.common, + com.sap.sailing.domain, + com.sap.sailing.domain.test, + com.sap.sse.mongodb, + com.mongodb.driver;bundle-version="2.6.2", + com.sap.sailing.domain.persistence, + com.sap.sailing.domain.tractracadapter, + com.tractrac.clientmodule;bundle-version="2.0.0", + com.sap.sailing.expeditionconnector, + com.sap.sailing.domain.swisstimingadapter, + org.json.simple;bundle-version="1.1.0", + org.junit4;bundle-version="4.8.2", + org.hamcrest;bundle-version="1.1.0", + com.sap.sailing.polars, + com.sap.sse.common, + org.mockito.mockito-core;bundle-version="1.9.5", + com.sap.sailing.domain.shared.android, + com.sap.sse.replication.testsupport diff --git a/java/com.sap.sailing.server.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.server.test/META-INF/MANIFEST.MF index d84e41d4b8e..ae3c881fe94 100644 --- a/java/com.sap.sailing.server.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.server.test/META-INF/MANIFEST.MF @@ -18,7 +18,7 @@ Require-Bundle: com.sap.sailing.domain, org.hamcrest;bundle-version="1.1.0", org.junit4;bundle-version="4.8.2", com.sap.sailing.domain.test, - org.mockito.mockito-core;bundle-version="1.9.5", + org.mockito.mockito-core;bundle-version="1.10.14", com.sap.sse.common, org.eclipse.jetty.server;bundle-version="9.2.2" Import-Package: com.sap.sailing.server.gateway.jaxrs, diff --git a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java index 33fde5c5fdd..8ba750b9c93 100755 --- a/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java +++ b/java/com.sap.sailing.server.test/src/com/sap/sailing/server/test/MasterDataImportTest.java @@ -137,12 +137,12 @@ public class MasterDataImportTest { private Set storedLogUUIDs = new HashSet(); @After - public void tearDown() throws MalformedURLException, IOException, InterruptedException { + public void tearDown() { deleteAllDataFromDatabase(); } @Before - public void setUp() throws MalformedURLException, IOException, InterruptedException { + public void setUp() { deleteAllDataFromDatabase(); } @@ -153,7 +153,7 @@ public class MasterDataImportTest { return spyResource; } - private void deleteAllDataFromDatabase() throws MalformedURLException, IOException, InterruptedException { + private void deleteAllDataFromDatabase() { MongoDBService service = MongoDBConfiguration.getDefaultTestConfiguration().getService(); service.getDB().getWriteConcern().fsync(); service.getDB().dropDatabase(); diff --git a/java/com.sap.sailing.server/META-INF/MANIFEST.MF b/java/com.sap.sailing.server/META-INF/MANIFEST.MF old mode 100644 new mode 100755 index baac64d4e59..b0dfb7a7927 --- a/java/com.sap.sailing.server/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.server/META-INF/MANIFEST.MF @@ -1,42 +1,45 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Sailing Server -Bundle-SymbolicName: com.sap.sailing.server -Bundle-Version: 1.0.0.qualifier -Bundle-Activator: com.sap.sailing.server.impl.Activator -Bundle-Vendor: SAP -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Import-Package: com.sap.sailing.server.gateway.deserialization, - org.json.simple, - org.json.simple.parser, - org.osgi.framework;version="1.3.0", - org.osgi.util.tracker;version="1.3.1" -Bundle-ActivationPolicy: lazy -Require-Bundle: com.sap.sailing.domain, - com.sap.sailing.expeditionconnector, - com.sap.sailing.declination, - com.sap.sse.mongodb, - com.mongodb.driver;bundle-version="2.6.2", - com.sap.sailing.domain.persistence, - com.sap.sailing.domain.common, - com.sap.sailing.udpconnector, - com.sap.sailing.domain.shared.android, - com.sap.sailing.server.gateway.serialization, - com.sap.sse.common, - com.sap.sse.operationaltransformation, - com.sap.sse.replication, - com.sap.sse -Bundle-ClassPath: . -Export-Package: com.sap.sailing.server, - com.sap.sailing.server.impl; - x-friends:="com.sap.sailing.mongodb.test, - com.sap.sailing.server.test, - com.sap.sailing.server.gateway.test, - com.sap.sailing.domain.swisstimingadapter.test, - com.sap.sailing.gwt.ui.test, - com.sap.sailing.server.replication.test, - com.sap.sailing.domain.racelogtrackingadapter.test", - com.sap.sailing.server.masterdata, - com.sap.sailing.server.operationaltransformation, - com.sap.sailing.server.racelog.tracking - +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Sailing Server +Bundle-SymbolicName: com.sap.sailing.server +Bundle-Version: 1.0.0.qualifier +Bundle-Activator: com.sap.sailing.server.impl.Activator +Bundle-Vendor: SAP +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Import-Package: com.sap.sailing.polars, + com.sap.sailing.polars.factory, + com.sap.sailing.server.gateway.deserialization, + org.json.simple, + org.json.simple.parser, + org.osgi.framework;version="1.3.0", + org.osgi.util.tracker;version="1.3.1" +Bundle-ActivationPolicy: lazy +Require-Bundle: com.sap.sailing.domain, + com.sap.sailing.expeditionconnector, + com.sap.sailing.declination, + com.sap.sse.mongodb, + com.mongodb.driver;bundle-version="2.6.2", + com.sap.sailing.domain.persistence, + com.sap.sailing.domain.common, + com.sap.sailing.udpconnector, + com.sap.sailing.domain.shared.android, + com.sap.sailing.server.gateway.serialization, + com.sap.sailing.polars, + com.sap.sse.common, + com.sap.sse.operationaltransformation, + com.sap.sse.replication, + com.sap.sse +Bundle-ClassPath: . +Export-Package: com.sap.sailing.server, + com.sap.sailing.server.impl; + x-friends:="com.sap.sailing.mongodb.test, + com.sap.sailing.server.test, + com.sap.sailing.server.gateway.test, + com.sap.sailing.domain.swisstimingadapter.test, + com.sap.sailing.gwt.ui.test, + com.sap.sailing.server.replication.test, + com.sap.sailing.domain.racelogtrackingadapter.test", + com.sap.sailing.server.masterdata, + com.sap.sailing.server.operationaltransformation, + com.sap.sailing.server.racelog.tracking + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy).launch b/java/com.sap.sailing.server/SailingServer (No Proxy).launch index abdcdda2e74..bbc1122bf7b 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy).launch @@ -1,37 +1,37 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch old mode 100644 new mode 100755 index 4c8b3a151ed..926aaa2e2e8 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Cached MTB).launch @@ -32,5 +32,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch old mode 100644 new mode 100755 index ec1be52c177..f57d39f89a9 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889).launch @@ -26,6 +26,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch old mode 100644 new mode 100755 index 70c1cc715ad..7945a99b661 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Jetty on 8889, Cached MTB).launch @@ -1,32 +1,32 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch index 301265e12cc..d481abb05d1 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, Remote Debug SAP VM).launch @@ -15,7 +15,7 @@ - + @@ -26,5 +26,5 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch b/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch old mode 100644 new mode 100755 index 65360e587c1..5d1e6d174b5 --- a/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy, winddbTest).launch @@ -1,36 +1,36 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy).launch b/java/com.sap.sailing.server/SailingServer (Proxy).launch old mode 100644 new mode 100755 index 9c7d360e12e..ffa2f32e837 --- a/java/com.sap.sailing.server/SailingServer (Proxy).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy).launch @@ -33,6 +33,6 @@ - + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch b/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch old mode 100644 new mode 100755 index a0182edfd34..1ddeba470d8 --- a/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, Jetty on 8889).launch @@ -1,31 +1,31 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch b/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch old mode 100644 new mode 100755 index d19d0d6849b..264906aaf2e --- a/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, Remote Debug SAP VM).launch @@ -1,30 +1,30 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch b/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch old mode 100644 new mode 100755 index 31a5acda5d5..d2c1d3d0301 --- a/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, SwissTiming Live Simulation).launch @@ -1,29 +1,29 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch b/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch old mode 100644 new mode 100755 index 9ac223baaab..74ee3b9ef10 --- a/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch +++ b/java/com.sap.sailing.server/SailingServer (Proxy, winddbTest).launch @@ -1,31 +1,31 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java index 61592d0411d..2d59cc512ca 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java @@ -62,6 +62,7 @@ import com.sap.sailing.domain.tracking.TrackedRegatta; import com.sap.sailing.domain.tracking.TrackedRegattaRegistry; import com.sap.sailing.domain.tracking.TrackerManager; import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sailing.polars.PolarDataService; import com.sap.sailing.server.masterdata.DataImportLockWithProgress; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; @@ -275,7 +276,7 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch */ void updateStoredLeaderboardGroup(LeaderboardGroup leaderboardGroup); - DynamicTrackedRace createTrackedRace(RegattaAndRaceIdentifier raceIdentifier, WindStore windStore, GPSFixStore gpsFixStore, + DynamicTrackedRace createTrackedRace(RegattaAndRaceIdentifier raceIdentifier, WindStore windStore, GPSFixStore gpsFixStore, long delayToLiveInMillis, long millisecondsOverWhichToAverageWind, long millisecondsOverWhichToAverageSpeed); Regatta getOrCreateDefaultRegatta(String name, String boatClassName, Serializable id); @@ -498,16 +499,18 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch WindStore getWindStore(); - GPSFixStore getGPSFixStore(); - - RaceTracker getRaceTrackerById(Object id); - - AbstractLogEventAuthor getServerAuthor(); - - CompetitorStore getCompetitorStore(); - - TypeBasedServiceFinderFactory getTypeBasedServiceFinderFactory(); - + PolarDataService getPolarDataService(); + + GPSFixStore getGPSFixStore(); + + RaceTracker getRaceTrackerById(Object id); + + AbstractLogEventAuthor getServerAuthor(); + + CompetitorStore getCompetitorStore(); + + TypeBasedServiceFinderFactory getTypeBasedServiceFinderFactory(); + /** * This lock exists to allow only one master data import at a time to avoid situation where multiple Imports * override each other in unpredictable fashion diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java index bda5d7786f8..3d3d4a824af 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java @@ -25,9 +25,12 @@ import java.util.Map.Entry; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; @@ -86,6 +89,7 @@ import com.sap.sailing.domain.common.RegattaIdentifier; import com.sap.sailing.domain.common.RegattaName; import com.sap.sailing.domain.common.Renamable; import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sailing.domain.common.TrackedRaceStatusEnum; import com.sap.sailing.domain.common.WindSource; import com.sap.sailing.domain.common.dto.FleetDTO; import com.sap.sailing.domain.common.dto.RegattaCreationParametersDTO; @@ -135,8 +139,11 @@ import com.sap.sailing.domain.tracking.Wind; import com.sap.sailing.domain.tracking.WindStore; import com.sap.sailing.domain.tracking.WindTracker; import com.sap.sailing.domain.tracking.WindTrackerFactory; +import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; import com.sap.sailing.domain.tracking.impl.DynamicTrackedRegattaImpl; import com.sap.sailing.expeditionconnector.ExpeditionWindTrackerFactory; +import com.sap.sailing.polars.PolarDataService; +import com.sap.sailing.polars.factory.PolarDataServiceFactory; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.server.Replicator; import com.sap.sailing.server.gateway.deserialization.impl.CourseAreaJsonDeserializer; @@ -330,6 +337,8 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes private final AbstractLogEventAuthor raceLogEventAuthorForServer = new LogEventAuthorImpl( RacingEventService.class.getName(), 0); + private final PolarDataService polarDataService; + /** * Allow only one master data import at a time to avoid situation where multiple Imports override each other in * unpredictable fashion @@ -351,6 +360,8 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes private ReplicationMasterDescriptor replicatingFromMaster; private Set> operationsSentToMasterForReplication; + + private ThreadLocal currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster = ThreadLocal.withInitial(() -> false); /** * Constructs a {@link DomainFactory base domain factory} that uses this object's {@link #competitorStore competitor @@ -471,6 +482,13 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes operationExecutionListeners = new ConcurrentHashMap<>(); courseListeners = new ConcurrentHashMap<>(); persistentRegattasForRaceIDs = new ConcurrentHashMap<>(); + final int THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors(), 3); + // TODO find out how many executors we have on server side and how to manage them + Executor polarExecutor = new ThreadPoolExecutor(/* corePoolSize */THREAD_POOL_SIZE, + /* maximumPoolSize */THREAD_POOL_SIZE, + /* keepAliveTime */60, TimeUnit.SECONDS, + /* workQueue */new LinkedBlockingQueue()); + polarDataService = PolarDataServiceFactory.createStandardPolarDataService(polarExecutor); this.raceLogReplicator = new RaceLogReplicator(this); this.raceLogScoringReplicator = new RaceLogScoringReplicator(this); this.mediaLibrary = new MediaLibrary(); @@ -501,6 +519,21 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes loadAllRemoteSailingServersAndSchedulePeriodicEventCacheRefresh(); } + @Override + public boolean isCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster() { + return currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster.get(); + } + + @Override + public void setCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster(boolean currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster) { + this.currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster.set(currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster); + } + + @Override + public PolarDataService getPolarDataService() { + return polarDataService; + } + @Override public void clearState() throws Exception { for (String leaderboardGroupName : new ArrayList<>(this.leaderboardGroupsByName.keySet())) { @@ -1317,6 +1350,9 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes * race. When a tracked race is removed, the {@link TrackedRaceReplicator} that was added as listener to that * tracked race is removed again. * + * A {@link PolarFixCacheUpdater} is added to every race so that polar fixes are aggregated when new GPS fixes + * arrive. + * * @author Axel Uhl (d043530) * */ @@ -1325,8 +1361,11 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes private final Map trackedRaceReplicators; + private final Map polarFixCacheUpdaters; + public RaceAdditionListener() { this.trackedRaceReplicators = new HashMap(); + this.polarFixCacheUpdaters = new HashMap(); } @Override @@ -1335,6 +1374,10 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes if (trackedRaceReplicator != null) { trackedRace.removeListener(trackedRaceReplicator); } + PolarFixCacheUpdater polarFixCacheUpdater = polarFixCacheUpdaters.remove(trackedRace); + if (polarFixCacheUpdater != null) { + trackedRace.removeListener(polarFixCacheUpdater); + } } @Override @@ -1355,9 +1398,28 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes TrackedRaceReplicator trackedRaceReplicator = new TrackedRaceReplicator(trackedRace); trackedRaceReplicators.put(trackedRace, trackedRaceReplicator); trackedRace.addListener(trackedRaceReplicator, /* fire wind already loaded */true, true); + + PolarFixCacheUpdater polarFixCacheUpdater = new PolarFixCacheUpdater(trackedRace); + polarFixCacheUpdaters.put(trackedRace, polarFixCacheUpdater); + trackedRace.addListener(polarFixCacheUpdater); } } + private class PolarFixCacheUpdater extends AbstractRaceChangeListener { + + private final TrackedRace race; + + public PolarFixCacheUpdater(TrackedRace race) { + this.race = race; + } + + @Override + public void competitorPositionChanged(GPSFixMoving fix, Competitor item) { + polarDataService.competitorPositionChanged(fix, item, race); + } + + } + private class TrackedRaceReplicator implements RaceChangeListener { private final TrackedRace trackedRace; @@ -1862,6 +1924,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes replicate(new TrackRegatta(regatta.getRegattaIdentifier())); regattaTrackingCache.put(regatta, result); ensureRegattaIsObservedForDefaultLeaderboardAndAutoLeaderboardLinking(result); + addRaceTrackingFinishedListenerForPolarFixCache(result); } return result; } finally { @@ -1869,6 +1932,28 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes } } + private void addRaceTrackingFinishedListenerForPolarFixCache(DynamicTrackedRegatta result) { + RaceListener raceListenerForPolarFixCacheUpdate = new RaceListener() { + @Override + public void raceRemoved(TrackedRace trackedRace) { + // TODO remove fixes from polar fix cache + } + + @Override + public void raceAdded(final TrackedRace trackedRace) { + trackedRace.addListener(new AbstractRaceChangeListener() { + @Override + public void statusChanged(TrackedRaceStatus newStatus) { + if (newStatus.getStatus() == TrackedRaceStatusEnum.FINISHED) { + polarDataService.newRaceFinishedTracking(trackedRace); + } + } + }); + } + }; + result.addRaceListener(raceListenerForPolarFixCacheUpdate); + } + @Override public DynamicTrackedRegatta getTrackedRegatta(com.sap.sailing.domain.base.Regatta regatta) { return regattaTrackingCache.get(regatta); diff --git a/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target b/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target old mode 100644 new mode 100755 index 17729d6cb13..96d86d00b19 --- a/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target +++ b/java/com.sap.sailing.targetplatform/definitions/race-analysis-p2-remote.target @@ -1,222 +1,222 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sse.datamining.shared/src/com/sap/sse/datamining/shared/impl/CompoundGroupKey.java b/java/com.sap.sse.datamining.shared/src/com/sap/sse/datamining/shared/impl/CompoundGroupKey.java index 6166caea99e..9198a29c107 100644 --- a/java/com.sap.sse.datamining.shared/src/com/sap/sse/datamining/shared/impl/CompoundGroupKey.java +++ b/java/com.sap.sse.datamining.shared/src/com/sap/sse/datamining/shared/impl/CompoundGroupKey.java @@ -4,6 +4,7 @@ import com.sap.sse.datamining.shared.GroupKey; public class CompoundGroupKey extends AbstractGroupKey { + private static final long serialVersionUID = -7902450253393172550L; private GroupKey mainKey; diff --git a/java/com.sap.sse.datamining/src/com/sap/sse/datamining/factories/GroupKeyFactory.java b/java/com.sap.sse.datamining/src/com/sap/sse/datamining/factories/GroupKeyFactory.java new file mode 100644 index 00000000000..5d487d0dcaa --- /dev/null +++ b/java/com.sap.sse.datamining/src/com/sap/sse/datamining/factories/GroupKeyFactory.java @@ -0,0 +1,26 @@ +package com.sap.sse.datamining.factories; + +import java.util.Iterator; + +import com.sap.sse.datamining.functions.Function; +import com.sap.sse.datamining.shared.GroupKey; +import com.sap.sse.datamining.shared.impl.CompoundGroupKey; +import com.sap.sse.datamining.shared.impl.GenericGroupKey; + +public class GroupKeyFactory { + + public static GroupKey createCompoundKeyFor(DataType input, Iterator> dimensionsIterator) { + Function mainDimension = dimensionsIterator.next(); + GroupKey key = createGroupKeyFor(input, mainDimension); + if (dimensionsIterator.hasNext()) { + key = new CompoundGroupKey(key, createCompoundKeyFor(input, dimensionsIterator)); + } + return key; + } + + public static GroupKey createGroupKeyFor(DataType input, Function mainDimension) { + Object keyValue = mainDimension.tryToInvoke(input); + return new GenericGroupKey(keyValue); + } + +} diff --git a/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/AbstractParallelMultiDimensionalGroupingProcessor.java b/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/AbstractParallelMultiDimensionalGroupingProcessor.java index 8a978eece6a..6f7ee032270 100644 --- a/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/AbstractParallelMultiDimensionalGroupingProcessor.java +++ b/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/AbstractParallelMultiDimensionalGroupingProcessor.java @@ -1,15 +1,14 @@ package com.sap.sse.datamining.impl.components; import java.util.Collection; -import java.util.Iterator; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import com.sap.sse.datamining.AdditionalResultDataBuilder; import com.sap.sse.datamining.components.Processor; +import com.sap.sse.datamining.factories.GroupKeyFactory; import com.sap.sse.datamining.functions.Function; import com.sap.sse.datamining.shared.GroupKey; -import com.sap.sse.datamining.shared.impl.CompoundGroupKey; public abstract class AbstractParallelMultiDimensionalGroupingProcessor extends AbstractSimpleParallelProcessor> { @@ -49,22 +48,13 @@ public abstract class AbstractParallelMultiDimensionalGroupingProcessor>() { @Override public GroupedDataEntry call() throws Exception { - return new GroupedDataEntry(createCompoundKeyFor(element, dimensions.iterator()), element); + return new GroupedDataEntry(GroupKeyFactory.createCompoundKeyFor(element, + dimensions.iterator()), element); } }; } - private GroupKey createCompoundKeyFor(DataType input, Iterator> dimensionsIterator) { - Function mainDimension = dimensionsIterator.next(); - GroupKey key = createGroupKeyFor(input, mainDimension); - if (dimensionsIterator.hasNext()) { - key = new CompoundGroupKey(key, createCompoundKeyFor(input, dimensionsIterator)); - } - return key; - } - protected abstract GroupKey createGroupKeyFor(DataType input, Function dimension); - @Override protected void setAdditionalData(AdditionalResultDataBuilder additionalDataBuilder) { } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/carousel/ImageCarousel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/carousel/ImageCarousel.java index 3d50bfd717e..51a6ed67a3a 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/carousel/ImageCarousel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/carousel/ImageCarousel.java @@ -58,7 +58,7 @@ public class ImageCarousel extends Widget { /** * slick slider property: centerMode */ - private boolean centerMode = true; + private boolean centerMode = false; /** * slick slider property: variableWidth */ @@ -112,7 +112,7 @@ public class ImageCarousel extends Widget { centerMode : (sliderReference.@com.sap.sse.gwt.client.controls.carousel.ImageCarousel::centerMode), variableWidth : (sliderReference.@com.sap.sse.gwt.client.controls.carousel.ImageCarousel::variableWidth), lazyLoad : (sliderReference.@com.sap.sse.gwt.client.controls.carousel.ImageCarousel::lazyload), - swipeToSlide : false, + swipeToSlide : true, arrows : true, responsive : true, slidesToShow : 2 @@ -138,6 +138,11 @@ public class ImageCarousel extends Widget { getElement().appendChild(imageHolder); + if (getElement().getChildCount() > 20) { + setInfiniteScrolling(false); + setShowDots(false); + } + } /** diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/carousel/ImageCarousel.ui.xml b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/carousel/ImageCarousel.ui.xml index e7c88bd4c6e..facd729c473 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/carousel/ImageCarousel.ui.xml +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/carousel/ImageCarousel.ui.xml @@ -9,11 +9,35 @@ .slider .slick-track { } + .slider li { + width: 10px; + padding: 2px; + } + + .slider .slick-dots li button:before { + width: 10px; + } + + .slider ul { + position: absolute; + bottom: -90px; + font-size: 10px; + overflow: hidden; + height: 70px; + } + + .slider .slick-dots li { + margin: 5px; + width: 10px; + } .slider ul > li > button:before { - font-size: 15px; + font-size: 10px; + width: 5spx; + padding: 0px; } + .slider .slick-slide { margin-left: 15px; margin-right: 15px; diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/Replicable.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/Replicable.java index 235bed6fc9c..ea6d7945aff 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/Replicable.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/Replicable.java @@ -172,12 +172,33 @@ public interface Replicable> extends Repl */ default void applyReceivedReplicated(OperationWithResult operation) { if (!hasSentOperationToMaster(operation)) { - applyReplicated(operation); + assert !isCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster(); + try { + setCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster(true); + applyReplicated(operation); + } finally { + setCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster(false); + } } else { logger.fine("Ignoring operation "+operation+" received back from master after having sent it there for execution and replication earlier"); } } + /** + * Responds with what has been passed to the last invocation to + * {@link #setCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster(boolean)} in the calling thread; + * the default is false. This is required in order to not replicate operations triggered on the replica + * while receiving the initial load back to the master. + */ + boolean isCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster(); + + /** + * {@link #isCurrentlyFillingFromInitialLoad} responds with what has been passed to the last invocation to this + * method in the calling thread; the default is false. This is required in order to not replicate + * operations triggered on the replica while receiving the initial load back to the master. + */ + void setCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster(boolean b); + /** * If an operation equal to operationWithResultWithIdWrapper has previously been passed to a call to * {@link #addOperationSentToMasterForReplication(OperationWithResultWithIdWrapper)}, the call returns true diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicableWithObjectInputStream.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicableWithObjectInputStream.java index e995c58077e..d90199d02a4 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicableWithObjectInputStream.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicableWithObjectInputStream.java @@ -65,15 +65,20 @@ public interface ReplicableWithObjectInputStream void replicate(O operation) { @SuppressWarnings("unchecked") final OperationWithResult castOperation = (OperationWithResult) operation; - if (getMasterDescriptor() != null) { + // if this is a replica and this replicable is not currently in the process of handling replication data (either an operation + // or the initial load) coming from the master, send the operation back to the master + if (getMasterDescriptor() != null && !isCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster()) { try { sendReplicaInitiatedOperationToMaster(castOperation); } catch (IOException e) { @@ -216,6 +223,7 @@ public interface ReplicableWithObjectInputStream T applyReplicated(OperationWithResult operation) { OperationWithResult reso = (OperationWithResult) operation; try { + setCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster(true); @SuppressWarnings("unchecked") S replicable = (S) this; T result = reso.internalApplyTo(replicable); @@ -226,6 +234,7 @@ public interface ReplicableWithObjectInputStream> operationsSentToMasterForReplication; + private ThreadLocal currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster = ThreadLocal.withInitial(() -> false); + private static Ini shiroConfiguration; static { shiroConfiguration = new Ini(); @@ -161,6 +163,16 @@ public class SecurityServiceImpl extends RemoteServiceServlet implements Replica this.securityManager = securityManager; } + @Override + public boolean isCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster() { + return currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster.get(); + } + + @Override + public void setCurrentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster(boolean currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster) { + this.currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster.set(currentlyFillingFromInitialLoadOrApplyingOperationReceivedFromMaster); + } + /** * Creates a default "admin" user with initial password "admin" and initial role "admin" if the user store * is empty. diff --git a/java/pom.xml b/java/pom.xml index 5fa6c19a9aa..10647e0f66c 100755 --- a/java/pom.xml +++ b/java/pom.xml @@ -122,6 +122,7 @@ com.sap.sailing.selenium.test com.sap.sailing.autoload com.sap.sailing.feature.p2build + com.sap.sailing.polars diff --git a/wiki/tracking-app-api-v1-draft.md b/wiki/tracking-app-api-v1-draft.md index 96b5f896058..1808b9f6985 100644 --- a/wiki/tracking-app-api-v1-draft.md +++ b/wiki/tracking-app-api-v1-draft.md @@ -64,6 +64,19 @@ see [leaderboards/{leaderboard_name}](http://www.sapsailing.com/sailingserver/we **Verb:** ``GET`` **Response:** + + + +
Proposed new formatOld format
+``` +{ + "name": "Heiko KRÖGER", + "id": "af855a56-9726-4a9c-a77e-da955bd289be", + "sailID": "GER 1", + "nationality": "GER", + "boatClassName": "49er", +} +``` + ``` { "name": "Heiko KRÖGER", @@ -74,6 +87,13 @@ see [leaderboards/{leaderboard_name}](http://www.sapsailing.com/sailingserver/we "countryCode": "DE" } ``` +
+ +**Changes** +* drop ``countryCode`` (is thee nationality field (3-letter IOC acronym) sufficient?) +* drop ``displayName`` (a competitor only has a potentially different displayName in the context of leaderboard -> we are not operating in such a context here) +* add ``boatClassName`` **Additional Notes:** * The semantics of ``displayName`` are explained in the JavaDoc of ``Leaderboard#getDisplayName()``.