mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-11-14 22:43:57 +00:00
Merge branch 'bug4134_2'
This commit is contained in:
commit
ef0587cf1c
14 files changed
+537
-211
No files matched your search
+1
@@ -6,6 +6,7 @@
|
||||
<div class="innerContent">
|
||||
<h5 class="articleSubheadline">August 2018</h5>
|
||||
<ul class="bulletList">
|
||||
<li>The windstreamlets are now colored dynamically according to the wind fields maximum and minimum wind speed.</li>
|
||||
<li>Changes of <tt>DataMining.html</tt>
|
||||
<ul class="bulletList">
|
||||
<li>The data type used by a statistic is now displayed by default.</li>
|
||||
|
||||
+85
-70
@@ -11,6 +11,7 @@ import com.google.gwt.canvas.dom.client.Context2d;
|
||||
import com.google.gwt.canvas.dom.client.TextMetrics;
|
||||
import com.google.gwt.core.client.Scheduler;
|
||||
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
|
||||
import com.google.gwt.i18n.client.NumberFormat;
|
||||
import com.google.gwt.maps.client.MapWidget;
|
||||
import com.google.gwt.maps.client.controls.ControlPosition;
|
||||
import com.google.gwt.user.client.rpc.AsyncCallback;
|
||||
@@ -18,6 +19,7 @@ import com.sap.sailing.domain.common.RegattaAndRaceIdentifier;
|
||||
import com.sap.sailing.domain.common.WindSource;
|
||||
import com.sap.sailing.domain.common.WindSourceType;
|
||||
import com.sap.sailing.gwt.ui.actions.GetWindInfoAction;
|
||||
import com.sap.sailing.gwt.ui.client.NumberFormatterFactory;
|
||||
import com.sap.sailing.gwt.ui.client.SailingServiceAsync;
|
||||
import com.sap.sailing.gwt.ui.client.StringMessages;
|
||||
import com.sap.sailing.gwt.ui.client.shared.racemap.CoordinateSystem;
|
||||
@@ -29,6 +31,7 @@ import com.sap.sailing.gwt.ui.simulator.racemap.MovingCanvasOverlay;
|
||||
import com.sap.sailing.gwt.ui.simulator.streamlets.Swarm;
|
||||
import com.sap.sailing.gwt.ui.simulator.streamlets.VectorField;
|
||||
import com.sap.sailing.gwt.ui.simulator.streamlets.WindInfoForRaceVectorField;
|
||||
import com.sap.sse.common.ColorMapperChangedListener;
|
||||
import com.sap.sse.common.Util;
|
||||
import com.sap.sse.gwt.client.Notification;
|
||||
import com.sap.sse.gwt.client.Notification.NotificationType;
|
||||
@@ -44,13 +47,12 @@ import com.sap.sse.gwt.client.player.Timer;
|
||||
* @author Axel Uhl (D043530)
|
||||
*
|
||||
*/
|
||||
public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
|
||||
public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay implements ColorMapperChangedListener{
|
||||
public static final String LOAD_WIND_STREAMLET_DATA_CATEGORY = "loadWindStreamletData";
|
||||
private static final int animationIntervalMillis = 40;
|
||||
private static final long RESOLUTION_IN_MILLIS = 5000;
|
||||
private static final int WIND_FETCH_INTERVAL_IN_MILLIS = 10000;
|
||||
private static final int CHECK_WIND_SOURCE_INTERVAL_IN_MILLIS = 60000;
|
||||
|
||||
private boolean visible = false;
|
||||
private final Timer timer;
|
||||
private Swarm swarm;
|
||||
@@ -63,9 +65,10 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
|
||||
private final Scheduler scheduler;
|
||||
private Canvas streamletLegend;
|
||||
private boolean firstColoring = true;
|
||||
|
||||
private boolean colored;
|
||||
private long latitudeCount;
|
||||
private double latitudeSum;
|
||||
private final NumberFormat numberFormatOneDecimal = NumberFormatterFactory.getDecimalFormat(1);
|
||||
|
||||
public WindStreamletsRaceboardOverlay(MapWidget map, int zIndex, final Timer timer,
|
||||
RegattaAndRaceIdentifier raceIdentifier, SailingServiceAsync sailingService,
|
||||
@@ -86,7 +89,7 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
|
||||
getCanvas().getElement().setId("swarm-display");
|
||||
createStreamletLegend(map);
|
||||
}
|
||||
|
||||
|
||||
public double getAverageLatitudeDeg() {
|
||||
return latitudeCount > 0 ? latitudeSum/latitudeCount : 0;
|
||||
}
|
||||
@@ -108,19 +111,6 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
|
||||
private void createStreamletLegend(MapWidget map) {
|
||||
streamletLegend = Canvas.createIfSupported();
|
||||
streamletLegend.addStyleName("MapStreamletLegend");
|
||||
//streamletLegend.setTitle(stringMessages.simulationLegendTooltip());
|
||||
/*streamletLegend.addClickHandler(new ClickHandler() {
|
||||
@Override
|
||||
public void onClick(ClickEvent event) {
|
||||
int clickPixelY = event.getRelativeY(streamletLegend.getElement());
|
||||
int legendRow = clickPixelY / ((int) rectHeight);
|
||||
int pathRow = legendRow - (racePath!=null ? 1 : 0);
|
||||
//Window.alert("clickPixelY: " + clickPixelY + "\nlegendRow: " + legendRow);
|
||||
visiblePaths[pathRow] = !visiblePaths[pathRow];
|
||||
clearCanvas();
|
||||
drawPaths();
|
||||
}
|
||||
});*/
|
||||
map.setControls(ControlPosition.LEFT_BOTTOM, streamletLegend);
|
||||
int canvasWidth = 500;
|
||||
int canvasHeight = 60;
|
||||
@@ -128,51 +118,69 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
|
||||
streamletLegend.setWidth(String.valueOf(canvasWidth));
|
||||
streamletLegend.setHeight(String.valueOf(canvasHeight));
|
||||
streamletLegend.setCoordinateSpaceWidth(canvasWidth);
|
||||
streamletLegend.setCoordinateSpaceHeight(canvasHeight); }
|
||||
streamletLegend.setCoordinateSpaceHeight(canvasHeight);
|
||||
}
|
||||
|
||||
private void drawLegend() {
|
||||
double x, y;
|
||||
x=100;
|
||||
y=16;
|
||||
double w = 1;
|
||||
double h = 20;
|
||||
int maxIdx = 300;
|
||||
Context2d context2d = streamletLegend.getContext2d();
|
||||
context2d.setFillStyle("rgba(0,0,0,.3)");
|
||||
context2d.setLineWidth(1.0);
|
||||
context2d.beginPath();
|
||||
context2d.fillRect(x-7.0, y-16.0, w*maxIdx+15.0, 56.0);
|
||||
context2d.closePath();
|
||||
context2d.stroke();
|
||||
context2d.setFillStyle("white");
|
||||
String label = stringMessages.windSpeedInKnots();
|
||||
TextMetrics txtmet;
|
||||
txtmet = context2d.measureText(label);
|
||||
context2d.fillText(label, x + (w*maxIdx - txtmet.getWidth())/2.0, y - 5.0);
|
||||
for(int idx=0; idx <= maxIdx; idx++) {
|
||||
//double speed = idx * 24.0 / maxIdx;
|
||||
double speed = 4.0 + idx * 16.0 / maxIdx;
|
||||
context2d.setFillStyle(windField.getColor(speed));
|
||||
context2d.beginPath();
|
||||
context2d.fillRect(x + idx*w, y, w, h);
|
||||
context2d.closePath();
|
||||
context2d.stroke();
|
||||
if (Math.abs(speed % 2.0) < (12.0/maxIdx)) {
|
||||
context2d.setStrokeStyle("white");
|
||||
public void drawLegend() {
|
||||
if (streamletLegend.isVisible()) {
|
||||
streamletLegend.getContext2d().clearRect(0, 0, streamletLegend.getCoordinateSpaceWidth(),
|
||||
streamletLegend.getCoordinateSpaceHeight());
|
||||
if (swarm != null && swarm.isColored()) {
|
||||
final double x = 100;
|
||||
final double y = 16;
|
||||
final double w = 1;
|
||||
final double h = 20;
|
||||
final double speed_max = swarm.getValueRange().getMaxRight();
|
||||
final double speed_min = swarm.getValueRange().getMinLeft();
|
||||
final double speed_spread = speed_max - speed_min;
|
||||
final int scale_spread;
|
||||
if (speed_spread < 0.5) {
|
||||
scale_spread = 300;
|
||||
}
|
||||
else if (speed_spread < 1) {
|
||||
scale_spread = 100;
|
||||
}
|
||||
else {
|
||||
scale_spread = 50;
|
||||
}
|
||||
final int maxIdx = 300;
|
||||
Context2d context2d = streamletLegend.getContext2d();
|
||||
context2d.setFillStyle("rgba(0,0,0,.3)");
|
||||
context2d.setLineWidth(1.0);
|
||||
context2d.beginPath();
|
||||
context2d.moveTo(x + idx*w, y + h);
|
||||
context2d.lineTo(x + idx*w, y + h + 7.0);
|
||||
context2d.fillRect(x - 10.0, y - 16.0, w * maxIdx + 20.0, 56.0);
|
||||
context2d.closePath();
|
||||
context2d.stroke();
|
||||
context2d.setFillStyle("white");
|
||||
label = ""+Math.round(speed);
|
||||
String label = stringMessages.windSpeedInKnots();
|
||||
TextMetrics txtmet;
|
||||
txtmet = context2d.measureText(label);
|
||||
context2d.fillText(label, x + idx*w - txtmet.getWidth()/2.0, y + h + 8.0 + 8.0);
|
||||
context2d.fillText(label, x + (w * maxIdx - txtmet.getWidth()) / 2.0, y - 5.0);
|
||||
for (int idx = 0; idx <= maxIdx; idx++) {
|
||||
final double speedSteps = speed_min + idx * (speed_max - speed_min) / maxIdx;
|
||||
context2d.setFillStyle(swarm.getColorMapper().getColor(speedSteps));
|
||||
context2d.beginPath();
|
||||
context2d.fillRect(x + idx * w, y, w, h);
|
||||
context2d.closePath();
|
||||
context2d.stroke();
|
||||
if (idx % scale_spread == 0) {
|
||||
context2d.setStrokeStyle("white");
|
||||
context2d.setLineWidth(1.0);
|
||||
context2d.beginPath();
|
||||
context2d.moveTo(x + idx * w, y + h);
|
||||
context2d.lineTo(x + idx * w, y + h + 7.0);
|
||||
context2d.closePath();
|
||||
context2d.stroke();
|
||||
context2d.setFillStyle("white");
|
||||
label = numberFormatOneDecimal.format(speedSteps);
|
||||
txtmet = context2d.measureText(label);
|
||||
context2d.fillText(label, x + idx * w - txtmet.getWidth() / 2.0, y + h + 8.0 + 8.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void startStreamlets() {
|
||||
scheduleWindDataRefresh();
|
||||
if (swarm == null) {
|
||||
@@ -180,9 +188,11 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
|
||||
this.swarm = new Swarm(this, map, timer, windField, new StreamletParameters());
|
||||
}
|
||||
initCanvasOrigin();
|
||||
this.swarm.setColors(colored);
|
||||
this.swarm.start(animationIntervalMillis);
|
||||
this.swarm.getColorMapper().addListener(this);
|
||||
}
|
||||
|
||||
|
||||
private void scheduleWindDataRefresh() {
|
||||
scheduler.scheduleFixedPeriod(new RepeatingCommand() {
|
||||
@Override
|
||||
@@ -201,10 +211,11 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
|
||||
// Now run things once, first updating the wind sources, then grabbing the wind from those sources:
|
||||
updateWindSourcesToObserve(new Runnable() { @Override public void run() { updateWindField(); } });
|
||||
}
|
||||
|
||||
|
||||
private void stopStreamlets() {
|
||||
if (swarm != null) {
|
||||
if (this.swarm != null) {
|
||||
this.swarm.stop();
|
||||
this.swarm.getColorMapper().removeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,10 +229,8 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
|
||||
sailingService.getWindSourcesInfo(raceIdentifier, new MarkedAsyncCallback<>(new AsyncCallback<WindInfoForRaceDTO>() {
|
||||
@Override
|
||||
public void onFailure(Throwable caught) {
|
||||
Notification.notify(stringMessages.errorFetchingWindStreamletData(caught.getMessage()),
|
||||
NotificationType.WARNING);
|
||||
Notification.notify(stringMessages.errorFetchingWindStreamletData(caught.getMessage()), NotificationType.WARNING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(WindInfoForRaceDTO result) {
|
||||
windInfoForRace.raceIsKnownToStartUpwind = result.raceIsKnownToStartUpwind;
|
||||
@@ -261,7 +270,6 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
|
||||
Notification.notify(stringMessages.errorFetchingWindStreamletData(caught.getMessage()),
|
||||
NotificationType.WARNING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(WindInfoForRaceDTO result) {
|
||||
updateAverageLatitudeDeg(result);
|
||||
@@ -288,23 +296,23 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isVisible() {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean isVisible) {
|
||||
if (getCanvas() != null) {
|
||||
if (isVisible) {
|
||||
if (this.windField.getColors()) {
|
||||
this.streamletLegend.setVisible(true);
|
||||
}
|
||||
this.startStreamlets();
|
||||
this.visible = isVisible;
|
||||
if (this.swarm.isColored()) {
|
||||
this.streamletLegend.setVisible(true);
|
||||
}
|
||||
} else {
|
||||
if (this.windField.getColors()) {
|
||||
if (this.swarm.isColored()) {
|
||||
this.streamletLegend.setVisible(false);
|
||||
}
|
||||
this.stopStreamlets();
|
||||
@@ -312,29 +320,36 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void setColors(boolean isColored) {
|
||||
this.windField.setColors(isColored);
|
||||
this.colored = isColored;
|
||||
if ((isColored) && (firstColoring)) {
|
||||
firstColoring = false;
|
||||
this.drawLegend();
|
||||
}
|
||||
this.streamletLegend.setVisible(isColored);
|
||||
if (swarm != null) {
|
||||
swarm.setColors(isColored);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFromMap() {
|
||||
this.setVisible(false);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void drawCenterChanged() {
|
||||
}
|
||||
|
||||
|
||||
public void onBoundsChanged(boolean zoomChanged) {
|
||||
if (swarm != null) {
|
||||
int swarmPause = (zoomChanged ? 5 : 1);
|
||||
swarm.onBoundsChanged(zoomChanged, swarmPause);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorMappingChanged() {
|
||||
drawLegend();
|
||||
}
|
||||
}
|
||||
-16
@@ -45,16 +45,12 @@ public class WindStreamletsCanvasOverlay extends FullCanvasOverlay implements Ti
|
||||
windFieldDTO = null;
|
||||
getCanvas().getElement().setId("swarm-display");
|
||||
}
|
||||
|
||||
public WindFieldDTO getWindFieldDTO() {
|
||||
return this.windFieldDTO;
|
||||
}
|
||||
|
||||
public WindFieldGenParamsDTO getWindParams() {
|
||||
return this.windParams;
|
||||
}
|
||||
|
||||
|
||||
public void startStreamlets() {
|
||||
if (swarm == null) {
|
||||
final SimulatorField field = new SimulatorField(getWindFieldDTO(), getWindParams(), streamletPars, coordinateSystem);
|
||||
@@ -63,27 +59,22 @@ public class WindStreamletsCanvasOverlay extends FullCanvasOverlay implements Ti
|
||||
}
|
||||
this.swarm.start(40);
|
||||
}
|
||||
|
||||
public void stopStreamlets() {
|
||||
if (swarm != null) {
|
||||
this.swarm.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void setEndDate(Date endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
public void setWindField(final WindFieldDTO windFieldDTO) {
|
||||
this.windFieldDTO = windFieldDTO;
|
||||
this.swarm = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVisible() {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean isVisible) {
|
||||
if (getCanvas() != null) {
|
||||
@@ -99,14 +90,12 @@ public class WindStreamletsCanvasOverlay extends FullCanvasOverlay implements Ti
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToMap() {
|
||||
if (timer != null) {
|
||||
timer.addTimeListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeFromMap() {
|
||||
if (timer != null) {
|
||||
@@ -114,11 +103,9 @@ public class WindStreamletsCanvasOverlay extends FullCanvasOverlay implements Ti
|
||||
}
|
||||
this.setVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void drawCenterChanged() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void draw() {
|
||||
super.draw();
|
||||
@@ -135,15 +122,12 @@ public class WindStreamletsCanvasOverlay extends FullCanvasOverlay implements Ti
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Swarm getSwarm() {
|
||||
return this.swarm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void timeChanged(final Date newDate, Date oldDate) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shallStop() {
|
||||
if (timer.getTime().getTime() >= this.endDate.getTime()) {
|
||||
|
||||
-22
@@ -29,13 +29,11 @@ public class RectField implements VectorField {
|
||||
|
||||
private final double maxLength;
|
||||
private final double particleFactor;
|
||||
private final String[] colorsForSpeeds;
|
||||
|
||||
private final CoordinateSystem coordinateSystem;
|
||||
|
||||
public RectField(Vector[][] field, double x0, double y0, double x1, double y1, StreamletParameters parameters, CoordinateSystem coordinateSystem) {
|
||||
this.coordinateSystem = coordinateSystem;
|
||||
colorsForSpeeds = createColorsForSpeeds();
|
||||
this.x0 = x0;
|
||||
this.x1 = x1;
|
||||
this.y0 = y0;
|
||||
@@ -147,26 +145,6 @@ public class RectField implements VectorField {
|
||||
public double getParticleWeight(LatLng p, Vector v) {
|
||||
return v == null ? 0 : (1.0 - v.length() / this.maxLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColor(double speed) {
|
||||
return colorsForSpeeds[getIntensity(speed)];
|
||||
}
|
||||
|
||||
private String[] createColorsForSpeeds() {
|
||||
String[] colors = new String[256];
|
||||
double alpha = 1.0;
|
||||
int greyValue = 255;
|
||||
for (int i = 0; i < 256; i++) {
|
||||
colors[i] = "rgba(" + (greyValue) + "," + (greyValue) + "," + (greyValue) + "," + (alpha * i / 255.0) + ")";
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
private int getIntensity(double speed) {
|
||||
double s = speed / maxLength;
|
||||
return (int) Math.min(255, 90 + Math.round(350 * s));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getLineWidth(double speed) {
|
||||
|
||||
-34
@@ -49,7 +49,6 @@ public class SimulatorField implements VectorField {
|
||||
private final Position nvX;
|
||||
|
||||
private final double[][][] data;
|
||||
private final String[] colorsForSpeeds;
|
||||
private final Date startTime;
|
||||
private final Duration timeStep;
|
||||
private final CoordinateSystem coordinateSystem;
|
||||
@@ -58,7 +57,6 @@ public class SimulatorField implements VectorField {
|
||||
this.coordinateSystem = coordinateSystem;
|
||||
this.startTime = windParams.getStartTime();
|
||||
this.timeStep = windParams.getTimeStep();
|
||||
this.colorsForSpeeds = createColorsForSpeeds();
|
||||
this.rcStart = new DegreePosition(windData.windData.rcStart.getLatDeg(), windData.windData.rcStart.getLngDeg());
|
||||
this.rcEnd = new DegreePosition(windData.windData.rcEnd.getLatDeg(), windData.windData.rcEnd.getLngDeg());
|
||||
this.resX = windData.windData.resX;
|
||||
@@ -228,38 +226,6 @@ public class SimulatorField implements VectorField {
|
||||
return v == null ? 0 : (v.length() / this.maxLength + 0.1);
|
||||
}
|
||||
|
||||
private String[] createColorsForSpeeds() {
|
||||
String[] colors = new String[256];
|
||||
double alphaMin = 0.0;
|
||||
double alphaMax = 0.9;
|
||||
int greyValue = 255;
|
||||
for (int i = 0; i < 256; i++) {
|
||||
colors[i] = "rgba(" + (greyValue) + "," + (greyValue) + "," + (greyValue) + ","
|
||||
+ (alphaMin + (alphaMax - alphaMin) * i / 255.0) + ")";
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColor(double speed) {
|
||||
return colorsForSpeeds[getIntensity(speed)];
|
||||
}
|
||||
|
||||
private int getIntensity(double speed) {
|
||||
/*
|
||||
* normalized intensity: speed == average wind speed => intensity 0.5 speed <= minimum wind speed => intensity
|
||||
* 0.0 speed between 0.0 and 1.0 double s; if (minLength == maxLength) { s = 0.5; } else if (speed <= minLength)
|
||||
* { s = 0.0; } else { s = (speed - minLength) / (maxLength - minLength); }
|
||||
*/
|
||||
|
||||
/*
|
||||
* absolute intensity speed == 12kn => intensity 0.7 speed == 20kn => intensity 1.0 speed == 0kn => intensity
|
||||
* 0.25
|
||||
*/
|
||||
double s = 0.7 + 0.0375 * (speed - 12.0);
|
||||
return (int) Math.max(0, Math.min(255, Math.round(255 * s)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getLineWidth(double speed) {
|
||||
/*
|
||||
|
||||
+51
-21
@@ -15,6 +15,8 @@ import com.sap.sailing.gwt.ui.client.shared.racemap.BoundsUtil;
|
||||
import com.sap.sailing.gwt.ui.client.shared.racemap.CoordinateSystem;
|
||||
import com.sap.sailing.gwt.ui.simulator.StreamletParameters;
|
||||
import com.sap.sailing.gwt.ui.simulator.racemap.FullCanvasOverlay;
|
||||
import com.sap.sse.common.ColorMapper;
|
||||
import com.sap.sse.common.ValueRangeFlexibleBoundaries;
|
||||
import com.sap.sse.gwt.client.player.TimeListener;
|
||||
|
||||
public class Swarm implements TimeListener {
|
||||
@@ -62,6 +64,10 @@ public class Swarm implements TimeListener {
|
||||
private double cosineOfAverageLatitude;
|
||||
|
||||
private HandlerRegistration handlerRegistration;
|
||||
|
||||
private boolean colored = false;
|
||||
private final ValueRangeFlexibleBoundaries valueRange;
|
||||
private final ColorMapper colorMapper;
|
||||
|
||||
public Swarm(FullCanvasOverlay fullcanvas, MapWidget map, com.sap.sse.gwt.client.player.Timer timer,
|
||||
VectorField vectorField, StreamletParameters streamletPars) {
|
||||
@@ -74,8 +80,10 @@ public class Swarm implements TimeListener {
|
||||
timePoint = timer.getTime();
|
||||
cosineOfAverageLatitude = 1.0; // default to equator
|
||||
diffPx = new Vector(0, 0);
|
||||
valueRange = new ValueRangeFlexibleBoundaries(/*wind speed in knots*/ 0.0,/*wind speed in knots*/ 60.0, /*percentage*/ 0.15);
|
||||
colorMapper = new ColorMapper(valueRange, !colored);
|
||||
}
|
||||
|
||||
|
||||
public void start(final int animationIntervalMillis) {
|
||||
fullcanvas.setCanvasSettings();
|
||||
// if map is not yet loaded, wait for it
|
||||
@@ -91,7 +99,7 @@ public class Swarm implements TimeListener {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void startWithMap(int animationIntervalMillis) {
|
||||
projection = new Mercator(fullcanvas, map);
|
||||
projection.calibrate();
|
||||
@@ -100,11 +108,10 @@ public class Swarm implements TimeListener {
|
||||
swarmContinue = true;
|
||||
startLoop(animationIntervalMillis);
|
||||
}
|
||||
|
||||
|
||||
public void stop() {
|
||||
swarmContinue = false;
|
||||
}
|
||||
|
||||
private Particle createParticle() {
|
||||
Particle particle = null;
|
||||
boolean done = false;
|
||||
@@ -113,7 +120,7 @@ public class Swarm implements TimeListener {
|
||||
while (!done && attempts-- > 0) {
|
||||
particle = new Particle();
|
||||
particle.currentPosition = getRandomPosition();
|
||||
if (field.inBounds(particle.currentPosition)) {
|
||||
if(field.inBounds(particle.currentPosition)) {
|
||||
Vector v = field.getVector(particle.currentPosition, timePoint);
|
||||
double weight = field.getParticleWeight(particle.currentPosition, v);
|
||||
if (weight >= Math.random()) {
|
||||
@@ -145,7 +152,7 @@ public class Swarm implements TimeListener {
|
||||
result = LatLng.newInstance(latDeg, lngDeg);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private Particle[] createParticles() {
|
||||
Particle[] newParticles = new Particle[nParticles];
|
||||
for (int idx = 0; idx < newParticles.length; idx++) {
|
||||
@@ -153,7 +160,7 @@ public class Swarm implements TimeListener {
|
||||
}
|
||||
return newParticles;
|
||||
}
|
||||
|
||||
|
||||
public void onBoundsChanged(boolean zoomChanged, int swarmPause) {
|
||||
this.zoomChanged |= zoomChanged;
|
||||
if (this.zoomChanged) {
|
||||
@@ -161,7 +168,7 @@ public class Swarm implements TimeListener {
|
||||
}
|
||||
this.swarmPause = swarmPause;
|
||||
}
|
||||
|
||||
|
||||
private void updateBounds() {
|
||||
LatLngBounds fieldBounds = this.field.getFieldCorners();
|
||||
final LatLngBounds mapBounds = map.getBounds();
|
||||
@@ -174,12 +181,8 @@ public class Swarm implements TimeListener {
|
||||
this.nParticles = (int)Math.round(Math.sqrt(boundsWidthpx * boundsHeightpx) * this.field.getParticleFactor() * this.parameters.swarmScale);
|
||||
cosineOfAverageLatitude = Math.cos((visibleBoundsOfField.getSouthWest().getLatitude()/180.*Math.PI+
|
||||
visibleBoundsOfField.getNorthEast().getLatitude()/180.*Math.PI)/2);
|
||||
};
|
||||
}
|
||||
|
||||
//native void log(String message) /*-{
|
||||
// console.log( message );
|
||||
//}-*/;
|
||||
|
||||
private void startLoop(final int animationIntervalMillis) {
|
||||
// Create animation-loop based on timer timeout
|
||||
loopTimer = new com.google.gwt.user.client.Timer() {
|
||||
@@ -217,7 +220,7 @@ public class Swarm implements TimeListener {
|
||||
};
|
||||
loopTimer.schedule(animationIntervalMillis);
|
||||
}
|
||||
|
||||
|
||||
private void drawSwarm() {
|
||||
Context2d ctxt = canvas.getContext2d();
|
||||
ctxt.setGlobalAlpha(0.08);
|
||||
@@ -234,19 +237,30 @@ public class Swarm implements TimeListener {
|
||||
}
|
||||
double particleSpeed = particle.v == null ? 0 : particle.v.length();
|
||||
ctxt.setLineWidth(field.getLineWidth(particleSpeed));
|
||||
ctxt.setStrokeStyle(field.getColor(particleSpeed));
|
||||
ctxt.setStrokeStyle(colorMapper.getColor(particleSpeed));
|
||||
ctxt.beginPath();
|
||||
ctxt.moveTo(particle.previousPixelCoordinate.x, particle.previousPixelCoordinate.y);
|
||||
ctxt.lineTo(particle.currentPixelCoordinate.x, particle.currentPixelCoordinate.y);
|
||||
ctxt.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void setColors(boolean isColored) {
|
||||
this.colored = isColored;
|
||||
colorMapper.setGrey(!isColored);
|
||||
}
|
||||
|
||||
public boolean isColored() {
|
||||
return colored;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves each particle by its vector {@link Particle#v} multiplied by the speed which is 0.01 times the
|
||||
* {@link VectorField#getMotionScale(int)} at the map's current zoom level.
|
||||
* {@link VectorField#getMotionScale(int)} at the map's current zoom level.
|
||||
*/
|
||||
private boolean execute(Vector diffPx) {
|
||||
double minSpeedInKnots = 120.0;
|
||||
double maxSpeedInKnots = 0.0;
|
||||
double speed = field.getMotionScale(map.getZoom());
|
||||
for (int idx = 0; idx < particles.length; idx++) {
|
||||
Particle particle = particles[idx];
|
||||
@@ -275,15 +289,31 @@ public class Swarm implements TimeListener {
|
||||
// particle timed out (age became 0) or was never created (e.g., weight too low); try to create a new one
|
||||
particles[idx] = this.createParticle();
|
||||
}
|
||||
if (particles[idx] != null && particles[idx].v != null) {
|
||||
final double length = particles[idx].v.length();
|
||||
if (length > maxSpeedInKnots) {
|
||||
maxSpeedInKnots = length;
|
||||
}
|
||||
if (length < minSpeedInKnots) {
|
||||
minSpeedInKnots = length;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (minSpeedInKnots <= maxSpeedInKnots) {
|
||||
valueRange.setMinMax(minSpeedInKnots, maxSpeedInKnots);
|
||||
}
|
||||
drawSwarm();
|
||||
return true;
|
||||
}
|
||||
|
||||
public VectorField getField() {
|
||||
return field;
|
||||
|
||||
public ValueRangeFlexibleBoundaries getValueRange() {
|
||||
return valueRange;
|
||||
}
|
||||
|
||||
|
||||
public ColorMapper getColorMapper() {
|
||||
return colorMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void timeChanged(Date newTime, Date oldTime) {
|
||||
timePoint = newTime;
|
||||
|
||||
-13
@@ -12,9 +12,6 @@ import com.sap.sailing.gwt.ui.client.shared.racemap.CoordinateSystem;
|
||||
* A field of vectors to display in a streamlet {@link Swarm}. The {@link Vector}s returned by {@link #getVector(Position, Date)}
|
||||
* are used to initialize {@link Particle}s. Their "speed" is the {@link Vector#length() length} of the vector.<p>
|
||||
*
|
||||
* This field provides color strings for each possible speed which are then used to style the particles as they fly through
|
||||
* the swarm.
|
||||
*
|
||||
* @author Christopher Ronnewinkel (D036654)
|
||||
* @author Axel Uhl (D043530)
|
||||
*
|
||||
@@ -83,14 +80,4 @@ public interface VectorField {
|
||||
LatLngBounds getFieldCorners();
|
||||
|
||||
double getParticleFactor();
|
||||
|
||||
/**
|
||||
* Computes a color for a particle flying at a certain speed.
|
||||
*
|
||||
* @param speed
|
||||
* a speed as obtained by computing a {@link Vector}'s {@link Vector#length() length}.
|
||||
* @return an RGB color with optional transparency that is a legal color in CSS. Example:
|
||||
* <code>"rgba(1, 2, 3, 0.4)"</code>
|
||||
*/
|
||||
String getColor(double speed);
|
||||
}
|
||||
-35
@@ -48,7 +48,6 @@ public class WindInfoForRaceVectorField implements VectorField, AverageLatitudeP
|
||||
private double averageLatitudeCosine;
|
||||
private double knotsInDegreePerFrame;
|
||||
private final CoordinateSystem coordinateSystem;
|
||||
private boolean colored = false;
|
||||
|
||||
private final Comparator<WindDTO> windByRequestTimePointComparator = new Comparator<WindDTO>() {
|
||||
@Override
|
||||
@@ -103,9 +102,6 @@ public class WindInfoForRaceVectorField implements VectorField, AverageLatitudeP
|
||||
double knotSpeedSumScaledByConfidence = 0;
|
||||
final BearingWithConfidenceCluster<Position> bearingCluster = new BearingWithConfidenceCluster<>(weigher);
|
||||
for (final Entry<WindSource, WindTrackInfoDTO> windSourceAndWindTrack : windInfoForRace.windTrackInfoByWindSource.entrySet()) {
|
||||
/*if ((windSourceAndWindTrack.getKey().name().equals("RACECOMMITTEE"))||(windSourceAndWindTrack.getKey().name().equals("COURSE_BASED"))) {
|
||||
continue;
|
||||
}*/
|
||||
if (!Util.contains(windInfoForRace.windSourcesToExclude, windSourceAndWindTrack.getKey())) {
|
||||
WindDTO timewiseClosestFixForWindSource = getTimewiseClosestFix(windSourceAndWindTrack.getValue(), at);
|
||||
if (timewiseClosestFixForWindSource != null) {
|
||||
@@ -215,35 +211,4 @@ public class WindInfoForRaceVectorField implements VectorField, AverageLatitudeP
|
||||
public double getParticleFactor() {
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
public void setColors(boolean isColored) {
|
||||
this.colored = isColored;
|
||||
}
|
||||
|
||||
public boolean getColors() {
|
||||
return this.colored;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link #MAX_WIND_SPEED_IN_KNOTS maximum wind speed} is fully opaque; zero speed is fully transparent
|
||||
*/
|
||||
@Override
|
||||
public String getColor(double speed) {
|
||||
if (this.colored) {
|
||||
double h;
|
||||
if (speed <= 4.0) {
|
||||
h = 240.0;
|
||||
} else if (speed <= 12.0) {
|
||||
h = 120.0 + (12.0 - speed) / (12.0 - 4.0) * (240.0 - 120.0);
|
||||
} else if (speed <= 20.0) {
|
||||
h = 0.0 + (20.0 - speed) / (20.0 - 12.0) * (120.0 - 0.0);
|
||||
} else {
|
||||
h = 0.0;
|
||||
}
|
||||
return "hsl(" + Math.round(h) + ", 100%, 50%)";
|
||||
} else {
|
||||
return "rgba(255,255,255," + Math.min(1.0, 0.5 + 0.6 * speed / MAX_WIND_SPEED_IN_KNOTS) + ")";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package com.sap.sse.common.test;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.sap.sse.common.ColorMapper;
|
||||
import com.sap.sse.common.ColorMapperChangedListener;
|
||||
import com.sap.sse.common.ValueRangeFlexibleBoundaries;
|
||||
|
||||
public class ColorMapperTest {
|
||||
private static final double EPSILON = 0.00000001;
|
||||
ColorMapper colorMapper;
|
||||
ColorMapperChangedListener listener;
|
||||
ValueRangeFlexibleBoundaries valueRange;
|
||||
Set<Integer> valueSet;
|
||||
Set<String> colorSet;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
valueRange = new ValueRangeFlexibleBoundaries(-5.0, 5.0, 0.0);
|
||||
colorMapper = new ColorMapper(valueRange, false);
|
||||
listener = mock(ColorMapperChangedListener.class);
|
||||
colorMapper.addListener(listener);
|
||||
valueSet = new HashSet<>();
|
||||
updateValueSet(valueSet);
|
||||
colorSet = new HashSet<>();
|
||||
fillColorSet();
|
||||
|
||||
}
|
||||
|
||||
public void updateValueSet(Set<Integer> valueSet) {
|
||||
valueSet.clear();
|
||||
for (int i = (int) Math.round(valueRange.getMinLeft()); i <= (int) Math.round(valueRange.getMaxRight()); i++) {
|
||||
valueSet.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
public void fillColorSet() {
|
||||
for (int i = 0; i <= 240; i++) {
|
||||
colorSet.add("hsl(" + i + ", 100%, 50%)");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForValueOutOfBoundaries() {
|
||||
assertTrue(colorMapper.getColor(valueRange.getMinLeft() - EPSILON) == "hsl(0, 100%, 50%)");
|
||||
assertTrue(colorMapper.getColor(valueRange.getMaxRight() + EPSILON) == "hsl(240, 100%, 50%)");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetColorColored() {
|
||||
// first set the color and verify listener will be notified
|
||||
colorMapper.setGrey(false);
|
||||
verify(listener, times(1)).onColorMappingChanged();
|
||||
// Check for each value in valueSet that getColor() returns a color within the colorSet.
|
||||
for (int i : valueSet) {
|
||||
assertTrue(colorSet.contains(colorMapper.getColor(i)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void changeValueRangeAndTestGetColorColored() {
|
||||
valueRange.setMinMax(12.0, 26.0);
|
||||
// verify Listener
|
||||
verify(listener, times(1)).onColorMappingChanged();
|
||||
updateValueSet(valueSet);
|
||||
// check if new values are within the colorSet
|
||||
for (int i : valueSet) {
|
||||
assertTrue(colorSet.contains(colorMapper.getColor(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
package com.sap.sse.common.test;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.sap.sse.common.ValueRangeFlexibleBoundaries;
|
||||
import com.sap.sse.common.ValueRangeFlexibleBoundariesChangedListener;
|
||||
|
||||
public class ValueRangeFlexibleBoundariesTest {
|
||||
private static final double EPSILON = 0.00000001;
|
||||
//Create new valueRange, boundaries should be [-6,-4] and [4,6].
|
||||
private ValueRangeFlexibleBoundaries valueRange;
|
||||
private double minLeft;
|
||||
private double maxRight;
|
||||
private ValueRangeFlexibleBoundariesChangedListener listener;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
// a ten percent range at each end of an original range of length 10, so length 1 at each end,
|
||||
// spread symmetrically around the min/max value
|
||||
valueRange = new ValueRangeFlexibleBoundaries(-5.0, 5.0, 0.10);
|
||||
minLeft = -6.0;
|
||||
maxRight = 6.0;
|
||||
listener = mock(ValueRangeFlexibleBoundariesChangedListener.class);
|
||||
valueRange.addListener(listener);
|
||||
verify(listener, times(0)).onValueRangeBoundariesChanged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExceptionForGreaterMinThanMax() {
|
||||
try {
|
||||
valueRange.setMinMax(6, 5);
|
||||
fail("Excpected IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckIfValueIsInLeftBoundaryRangeAndUpdateIfNecessary() {
|
||||
// update left border to 0.0, that means boundaries now should be [-0.5,0.5] and [4.5,5.5]
|
||||
// because the new range has length 5, so 10% of that is .5, so the left range is -0.5..0.5
|
||||
// and the right range is 4.5..5.5. We expect the update to be delivered by exactly one
|
||||
// call to the listener
|
||||
valueRange.setMinMax(0.0, 5.0);
|
||||
double newMinLeft = -0.5;
|
||||
double newMaxRight = 5.5;
|
||||
assertEquals(newMinLeft, valueRange.getMinLeft(), EPSILON);
|
||||
assertEquals(newMaxRight, valueRange.getMaxRight(), EPSILON);
|
||||
verify(listener, times(1)).onValueRangeBoundariesChanged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateWithinBoundaries() {
|
||||
valueRange.setMinMax(-5.5, 5.5);
|
||||
verify(listener, times(0)).onValueRangeBoundariesChanged();
|
||||
valueRange.setMinMax(-4.5, 4.5);
|
||||
verify(listener, times(0)).onValueRangeBoundariesChanged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGradualUpdateCrossingBoundariesEventuallyTriggerUpdate() {
|
||||
valueRange.setMinMax(-5.5, 5.0);
|
||||
verify(listener, times(0)).onValueRangeBoundariesChanged();
|
||||
valueRange.setMinMax(-6-EPSILON, 5.0); // just barely cross the *original* range boundary to the left,
|
||||
// thus asserting that it wasn't expanded earlier by coming closer (from -5.0 to -5.5) to its end
|
||||
verify(listener, times(1)).onValueRangeBoundariesChanged();
|
||||
verify(listener, times(1)).onValueRangeBoundariesChanged(); // ensure that calling verify doesn't clean counter
|
||||
// and now it *was* expanded to cover -6..5, so the tolerance has become slightly greater, and 6+EPSILON
|
||||
// should fit into the new bounds easily
|
||||
valueRange.setMinMax(-6-EPSILON, 6+EPSILON);
|
||||
verify(listener, times(1)).onValueRangeBoundariesChanged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckIfValueIsInRightBoundaryRangeAndUpdateIfNecessary() {
|
||||
// update right border to 20, that means the boundaries now should be [-7.5,-2.5] and [17.5,22.5]
|
||||
valueRange.setMinMax(-5.0, 20.0);
|
||||
Double newMaxRight = 22.5;
|
||||
Double newMinLeft = -7.5;
|
||||
assertEquals(newMaxRight, (Double) valueRange.getMaxRight());
|
||||
assertEquals(newMinLeft, (Double) valueRange.getMinLeft());
|
||||
verify(listener, times(1)).onValueRangeBoundariesChanged();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMinLeft() {
|
||||
assertEquals(minLeft, valueRange.getMinLeft(), EPSILON);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMaxRight() {
|
||||
assertEquals(maxRight, valueRange.getMaxRight(), EPSILON);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.sap.sse.common;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Maps a given range of values to a full color spectrum. The color spectrum could also be a grey scale. Therefore it
|
||||
* needs the max and the min value to be mapped. It uses a {@link ValueRangeFlexibleBoundaries} to get those. The
|
||||
* ColorMapper has listeners that will be notified if the colormapping has been changed.
|
||||
*
|
||||
* @author D073259 (Alessandro Stoltenberg)
|
||||
*
|
||||
*/
|
||||
public class ColorMapper implements ValueRangeFlexibleBoundariesChangedListener {
|
||||
private final ValueRangeFlexibleBoundaries valueRange;
|
||||
private double minValue;
|
||||
private double maxValue;
|
||||
private boolean isGrey;
|
||||
private final Set<ColorMapperChangedListener> colorMapperChangedListeners;
|
||||
|
||||
public ColorMapper(ValueRangeFlexibleBoundaries valueRange, boolean isGrey) {
|
||||
this.valueRange = valueRange;
|
||||
this.valueRange.addListener(this);
|
||||
minValue = this.valueRange.getMinLeft();
|
||||
maxValue = this.valueRange.getMaxRight();
|
||||
this.isGrey = isGrey;
|
||||
colorMapperChangedListeners = new HashSet<>();
|
||||
}
|
||||
|
||||
public void setGrey(boolean isGrey) {
|
||||
this.isGrey = isGrey;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string with a rgba representation of a the value mapped to the color spectrum in case {@link #isGrey}
|
||||
* <c> = true </c> or the hsl representation of the color in case {@link #isGrey} <c> = false </c>. If called with a
|
||||
* value outside the {@link #valueRange} the return will be the minimum or the maximum of the spectrum, depending if
|
||||
* the input value is smaller or greater then the {@link #minValue} or {@link #maxValue} of the {@link #valueRange}.
|
||||
*/
|
||||
public String getColor(double value) {
|
||||
final String result;
|
||||
if (isGrey) {
|
||||
if (value < minValue) {
|
||||
result = "rgba(255,255,255,0.0)";
|
||||
} else if (value > maxValue) {
|
||||
result = "rgba(255,255,255,1.0)";
|
||||
} else {
|
||||
result = "rgba(255,255,255," + Math.min(1.0, (value - minValue) / (maxValue - minValue)) + ")";
|
||||
}
|
||||
} else {
|
||||
if (value < minValue) {
|
||||
result = "hsl(0, 100%, 50%)";
|
||||
} else if (value > maxValue) {
|
||||
result = "hsl(240, 100%, 50%)";
|
||||
} else {
|
||||
double h = (1 - (value - minValue) / (maxValue - minValue)) * 240;
|
||||
result = "hsl(" + Math.round(h) + ", 100%, 50%)";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void updateMinMax() {
|
||||
minValue = valueRange.getMinLeft();
|
||||
maxValue = valueRange.getMaxRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onValueRangeBoundariesChanged() {
|
||||
updateMinMax();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
public void addListener(ColorMapperChangedListener listener) {
|
||||
colorMapperChangedListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(ColorMapperChangedListener listener) {
|
||||
colorMapperChangedListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void notifyListeners() {
|
||||
for (ColorMapperChangedListener listener : colorMapperChangedListeners) {
|
||||
listener.onColorMappingChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.sap.sse.common;
|
||||
|
||||
public interface ColorMapperChangedListener {
|
||||
void onColorMappingChanged();
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package com.sap.sse.common;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Manages a number range that contains a minimum and a maximum value. Those minimum and maximum values can be updated.
|
||||
* If at least one of the two new values is outside of the range, the range is adjusted to ensure it again contains both
|
||||
* values. The range will also shrink if the new minimum/maximum values are too far away from the range boundaries. This
|
||||
* compares to a "Schmitt Trigger" for each end of the range, such that for the minimum and the maximum value there is
|
||||
* each a value range at the respective end of the overall range, and when the minimum/maximum value is outside of its
|
||||
* range the overall range will be adjusted.
|
||||
* <p>
|
||||
*
|
||||
* When the range is initialized or updated, the ranges for the minimum and the maximum value at each end of the overall
|
||||
* range are determined symmetrically around the minimum/maximum value, with a given {@link #percentage} of the absolute
|
||||
* difference between the minimum and maximum value as leeway either way. This percentage is provided to this class's
|
||||
* constructor. For example, if the overall range is -5..5, its length is 10. If a 10% (0.1) leeway is configured, this
|
||||
* means that 10% of the length of 10 which equals 1 is provided around each extremal value, yielding a range from
|
||||
* -6..-4 around the minimum value -5, and a range from 4..6 around the maximum value 5.
|
||||
* <p>
|
||||
*
|
||||
* Visually, if we have:
|
||||
* <p>
|
||||
*
|
||||
* a) minLeft<br>
|
||||
* b) maxLeft<br>
|
||||
* c) minRight<br>
|
||||
* d) maxRight
|
||||
* <p>
|
||||
* then the range can be imagined like this:
|
||||
* <p>
|
||||
*
|
||||
* (a)<--minValue-->(b)..........(c)<--maxValue-->(d)
|
||||
* <p>
|
||||
*
|
||||
* If an extremal value is not within the left or the right boundaries the boundaries will be adjusted as described
|
||||
* above, and all {@link ValueRangeFlexibleBoundariesChangedListener listeners} that were
|
||||
* {@link #addListener(ValueRangeFlexibleBoundariesChangedListener) added} will be notified.
|
||||
* <p>
|
||||
*
|
||||
* A ValueRangeFlexibleBoundaries needs an initialMin, an initialMax and a percentage that indicates the width of the
|
||||
* value range surrounding initialMin and Max.
|
||||
*
|
||||
* @author D073259 (Alessandro Stoltenberg)
|
||||
*
|
||||
*/
|
||||
public class ValueRangeFlexibleBoundaries {
|
||||
private double minLeft;
|
||||
private double maxLeft;
|
||||
private double minRight;
|
||||
private double maxRight;
|
||||
private double min;
|
||||
private double max;
|
||||
private final double percentage;
|
||||
private double halfBoundaryWidth;
|
||||
private final Set<ValueRangeFlexibleBoundariesChangedListener> valueRangeChangedListeners;
|
||||
|
||||
public ValueRangeFlexibleBoundaries(double initialMin, double initialMax, double percentage) {
|
||||
this.percentage = percentage;
|
||||
valueRangeChangedListeners = new HashSet<>();
|
||||
min = initialMin;
|
||||
max = initialMax;
|
||||
update();
|
||||
}
|
||||
|
||||
private void update() {
|
||||
halfBoundaryWidth = (max - min) * (percentage);
|
||||
minLeft = min - halfBoundaryWidth;
|
||||
maxLeft = min + halfBoundaryWidth;
|
||||
minRight = max - halfBoundaryWidth;
|
||||
maxRight = max + halfBoundaryWidth;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the minimum and maximum value. If {@code minValue} is greater than {@code maxValue},
|
||||
* an {@link IllegalArgumentException} will be thrown. If any of the values exceeds its range at
|
||||
* its respective end of this overall range then the overall range will be adjusted, and the
|
||||
* listeners will be notified.
|
||||
*/
|
||||
public void setMinMax(double minValue, double maxValue) {
|
||||
if (minValue > maxValue) {
|
||||
throw new IllegalArgumentException("minValue "+minValue+" is greater than maxValue "+maxValue);
|
||||
}
|
||||
min = minValue;
|
||||
max = maxValue;
|
||||
if (!isValueInRightRange(maxValue) || !isValueInLeftRange(minValue)) {
|
||||
update();
|
||||
}
|
||||
assert minLeft <= minValue;
|
||||
assert minValue <= maxLeft;
|
||||
assert minRight <= maxValue;
|
||||
assert maxValue <= maxRight;
|
||||
}
|
||||
|
||||
private boolean isValueInLeftRange(double value) {
|
||||
return value >= minLeft && value <= maxLeft;
|
||||
}
|
||||
|
||||
private boolean isValueInRightRange(double value) {
|
||||
return value >= minRight && value <= maxRight;
|
||||
}
|
||||
|
||||
public double getMinLeft() {
|
||||
return minLeft;
|
||||
}
|
||||
|
||||
public double getMaxRight() {
|
||||
return maxRight;
|
||||
}
|
||||
|
||||
public void addListener(ValueRangeFlexibleBoundariesChangedListener listener) {
|
||||
valueRangeChangedListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeListener(ValueRangeFlexibleBoundariesChangedListener listener) {
|
||||
valueRangeChangedListeners.remove(listener);
|
||||
}
|
||||
|
||||
public void notifyListeners() {
|
||||
for (ValueRangeFlexibleBoundariesChangedListener listener : valueRangeChangedListeners) {
|
||||
listener.onValueRangeBoundariesChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
package com.sap.sse.common;
|
||||
|
||||
public interface ValueRangeFlexibleBoundariesChangedListener {
|
||||
void onValueRangeBoundariesChanged();
|
||||
}
|
||||
Reference in new issue
Block a user