Merge branch 'bug4134_2'

This commit is contained in:
Alessandro Stoltenberg committed 2018-08-22 10:47:33 +02:00
commit ef0587cf1c
14 files changed
+537 -211

No files matched your search

@@ -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>
@@ -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();
}
}
@@ -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()) {
@@ -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) {
@@ -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) {
/*
@@ -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;
@@ -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);
}
@@ -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) + ")";
}
}
}
@@ -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)));
}
}
}
@@ -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();
}
}
}
@@ -0,0 +1,5 @@
package com.sap.sse.common;
public interface ColorMapperChangedListener {
void onColorMappingChanged();
}
@@ -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)&lt;--minValue--&gt;(b)..........(c)&lt;--maxValue--&gt;(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();
}
}
}
@@ -0,0 +1,5 @@
package com.sap.sse.common;
public interface ValueRangeFlexibleBoundariesChangedListener {
void onValueRangeBoundariesChanged();
}