bug4134_2: A working version of colored windstreamlets according to minmax speed.

This commit is contained in:
Alessandro Stoltenberg
2018-07-20 16:33:11 +02:00
parent 628ddf5470
commit 46dd886200
4 changed files with 200 additions and 46 deletions
@@ -0,0 +1,5 @@
package com.sap.sailing.gwt.ui.shared.racemap;
public interface SwarmMinMaxSpeedChangedListener {
void onSwarmMinMaxSpeedChanged();
}
@@ -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;
@@ -44,7 +46,7 @@ import com.sap.sse.gwt.client.player.Timer;
* @author Axel Uhl (D043530)
*
*/
public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay implements SwarmMinMaxSpeedChangedListener{
public static final String LOAD_WIND_STREAMLET_DATA_CATEGORY = "loadWindStreamletData";
private static final int animationIntervalMillis = 40;
private static final long RESOLUTION_IN_MILLIS = 5000;
@@ -66,6 +68,8 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
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,
@@ -130,45 +134,62 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
streamletLegend.setCoordinateSpaceWidth(canvasWidth);
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.getMaxParticleSpeedInKnots();
final double speed_min = swarm.getMinParticleSpeedInKnots();
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.getColorWithSpectrumBoundaries(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);
}
}
}
}
}
@@ -180,7 +201,9 @@ 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.addSwarmMinMaxSpeedChangedListener(this);
}
private void scheduleWindDataRefresh() {
@@ -203,8 +226,9 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
}
private void stopStreamlets() {
if (swarm != null) {
if (this.swarm != null) {
this.swarm.stop();
this.swarm.removeSwarmMinMaxSpeedChangedListener(this);
}
}
@@ -298,13 +322,13 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
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,14 +336,15 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
}
}
}
private boolean colored;
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
@@ -337,4 +362,8 @@ public class WindStreamletsRaceboardOverlay extends MovingCanvasOverlay {
swarm.onBoundsChanged(zoomChanged, swarmPause);
}
}
public void onSwarmMinMaxSpeedChanged() {
drawLegend();
}
}
@@ -0,0 +1,38 @@
package com.sap.sailing.gwt.ui.simulator.streamlets;
public class SchmittTrigger {
private double percentage;
public SchmittTrigger() {}
public SchmittTrigger(double percentageTriggerThreeshold) {
this.percentage = percentageTriggerThreeshold;
}
public boolean isGreaterThanThreeshold(double input,double threesholdValue) {
if (input * (1 + percentage/100) > threesholdValue) {
return true;
}
else {
return false;
}
}
public boolean isLesserThanThreeshold(double input,double threesholdValue) {
if (input * (1 - percentage/100) < threesholdValue) {
return true;
}
else {
return false;
}
}
public boolean isValueChangeTriggered(double input, double threesholdValue) {
if (isGreaterThanThreeshold(input, threesholdValue) || isLesserThanThreeshold(input, threesholdValue)) {
return true;
}
else {
return false;
}
}
public double getTriggerPercentage() {
return percentage;
}
}
@@ -1,6 +1,8 @@
package com.sap.sailing.gwt.ui.simulator.streamlets;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.Context2d;
@@ -13,6 +15,7 @@ import com.google.gwt.maps.client.events.bounds.BoundsChangeMapHandler;
import com.google.gwt.user.client.Timer;
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.shared.racemap.SwarmMinMaxSpeedChangedListener;
import com.sap.sailing.gwt.ui.simulator.StreamletParameters;
import com.sap.sailing.gwt.ui.simulator.racemap.FullCanvasOverlay;
import com.sap.sse.gwt.client.player.TimeListener;
@@ -74,6 +77,8 @@ public class Swarm implements TimeListener {
timePoint = timer.getTime();
cosineOfAverageLatitude = 1.0; // default to equator
diffPx = new Vector(0, 0);
swarmMinMaxSpeedChangedListeners = new HashSet<>();
schmittTrigger = new SchmittTrigger(/*percent*/ 25);
}
public void start(final int animationIntervalMillis) {
@@ -113,7 +118,9 @@ public class Swarm implements TimeListener {
while (!done && attempts-- > 0) {
particle = new Particle();
particle.currentPosition = getRandomPosition();
if (field.inBounds(particle.currentPosition)) {
// Is the if check really needed? getRandomPosition() returns always a position that is within the
// visibleBoundsOfField. visibleBoundsOfField is always within fieldBounds. See updateBounds().
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,6 @@ 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++) {
@@ -234,7 +240,7 @@ 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(getColorWithSpectrumBoundaries(particleSpeed));
ctxt.beginPath();
ctxt.moveTo(particle.previousPixelCoordinate.x, particle.previousPixelCoordinate.y);
ctxt.lineTo(particle.currentPixelCoordinate.x, particle.currentPixelCoordinate.y);
@@ -242,11 +248,61 @@ public class Swarm implements TimeListener {
}
}
private boolean colored = false;
public String getColorWithSpectrumBoundaries(double speed) {
if (colored) {
double h = (1 - (speed - minParticleSpeedInKnots)/(maxParticleSpeedInKnots - minParticleSpeedInKnots)) * 240;
return "hsl(" + Math.round(h) + ", 100%, 50%)";
} else {
return "rgba(255,255,255," + Math.min(1.0, speed / 40) + ")";
}
}
public void setColors(boolean isColored) {
this.colored = isColored;
}
public boolean isColored() {
return colored;
}
private double maxParticleSpeedInKnots = 0.0;
/**
* @return the maxParticleSpeedInKnots
*/
public double getMaxParticleSpeedInKnots() {
return maxParticleSpeedInKnots;
}
/**
* @return the minParticleSpeedInKnots
*/
public double getMinParticleSpeedInKnots() {
return minParticleSpeedInKnots;
}
private double minParticleSpeedInKnots = 120.0;
private boolean isNotifyListeners = true;
private void notifyListeners() {
if (isNotifyListeners) {
for (SwarmMinMaxSpeedChangedListener listener : swarmMinMaxSpeedChangedListeners) {
listener.onSwarmMinMaxSpeedChanged();
}
}
}
private final SchmittTrigger schmittTrigger;
/**
* 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. Also the max and min speed fields of the
* swarm get updated and the listeners get notified.
*/
private boolean execute(Vector diffPx) {
boolean isUpdateListeners = false;
double minSpeed = 120.0;
double maxSpeed = 0.0;
double speed = field.getMotionScale(map.getZoom());
for (int idx = 0; idx < particles.length; idx++) {
Particle particle = particles[idx];
@@ -268,6 +324,12 @@ public class Swarm implements TimeListener {
particle.stepsToLive--;
if ((particle.stepsToLive > 0) && (this.field.inBounds(particle.currentPosition))) {
particle.v = field.getVector(particle.currentPosition, timePoint);
if (particle.v.length() > maxSpeed) {
maxSpeed = particle.v.length();
}
if (particle.v.length() < minSpeed) {
minSpeed = particle.v.length();
}
} else {
particle.v = null;
}
@@ -276,7 +338,17 @@ public class Swarm implements TimeListener {
particles[idx] = this.createParticle();
}
}
if (schmittTrigger.isValueChangeTriggered(maxSpeed, maxParticleSpeedInKnots)) {
maxParticleSpeedInKnots = maxSpeed;
isUpdateListeners = true;
}
if (schmittTrigger.isValueChangeTriggered(minSpeed, minParticleSpeedInKnots)) {
minParticleSpeedInKnots = minSpeed;
isUpdateListeners = true;
}
this.isNotifyListeners = isUpdateListeners;
drawSwarm();
notifyListeners();
return true;
}
@@ -288,4 +360,14 @@ public class Swarm implements TimeListener {
public void timeChanged(Date newTime, Date oldTime) {
timePoint = newTime;
}
private final Set<SwarmMinMaxSpeedChangedListener> swarmMinMaxSpeedChangedListeners;
public void addSwarmMinMaxSpeedChangedListener(SwarmMinMaxSpeedChangedListener listener) {
swarmMinMaxSpeedChangedListeners.add(listener);
}
public void removeSwarmMinMaxSpeedChangedListener(SwarmMinMaxSpeedChangedListener listener) {
swarmMinMaxSpeedChangedListeners.remove(listener);
}
}