Merge remote-tracking branch 'origin/story1266_3d_model_factory' into NewUI_merge

# Conflicts:
#	pom.xml
#	src/main/java/seng302/App.java
#	src/main/java/seng302/gameServer/GameState.java
#	src/main/java/seng302/gameServer/MainServerThread.java
#	src/main/java/seng302/gameServer/ServerToClientThread.java
#	src/main/java/seng302/utilities/XMLGenerator.java
#	src/main/java/seng302/visualiser/GameClient.java
#	src/main/java/seng302/visualiser/controllers/RaceViewController.java
#	src/main/resources/views/RaceView.fxml
#	src/main/resources/views/StartScreenView.fxml
This commit is contained in:
Michael Rausch
2017-09-11 15:29:33 +12:00
130 changed files with 24554 additions and 331 deletions
@@ -0,0 +1,230 @@
package seng302.visualiser.fxObjects.assets_2D;
import java.util.HashMap;
import java.util.Map;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
/**
* Grouping of string objects over a semi transparent background.
*/
public class AnnotationBox extends Group {
@FunctionalInterface
public interface AnnotationFormatter<T> {
String transformString (T input);
}
/**
* Class stores a text object and relationship for updating the text object if needed
*
* @param <T> The type of observable value passed to the annotation, if there is one.
*/
public class Annotation<T> {
private Text text;
private ObservableValue<T> source;
private AnnotationFormatter<T> format;
/**
* Constructor for observing annotation
* @param textObject the javaFX text object the annotation is displayed in
* @param source observable value that the annotation is taken from
* @param formatter interface describing how to format the source data if needed
*/
public Annotation (Text textObject, ObservableValue<T> source, AnnotationFormatter<T> formatter) {
this.text = textObject;
this.source = source;
this.format = formatter;
source.addListener((obs, oldVal, newVal) ->
Platform.runLater(() -> text.setText(format.transformString(newVal)))
);
}
/**
* Constructor for a static annotation
* @param textObject the javaFX text object the annotation is displayed in
* @param annotationText the static value of the test object
*/
public Annotation (Text textObject, String annotationText) {
textObject.setText(annotationText);
text = textObject;
}
private Text getText () {
return text;
}
}
//Text offset constants
private static final double X_OFFSET_TEXT = 20d;
private static final double Y_OFFSET_TEXT_INIT = -35d;
private static final double Y_OFFSET_PER_TEXT = 12d;
//Background constants
private static final double TEXT_BUFFER = 3;
private static final double BACKGROUND_X = X_OFFSET_TEXT - TEXT_BUFFER;
private static final double BACKGROUND_Y = Y_OFFSET_TEXT_INIT - TEXT_BUFFER;
private static final double BACKGROUND_H_PER_TEXT = 9.5d;
private static final double BACKGROUND_ARC_SIZE = 10;
private int visibleAnnotations = 0;
private double backgroundWidth = 145d;
private Rectangle background = new Rectangle();
private Paint theme = Color.BLACK;
private Map<String, Annotation> annotationsByName = new HashMap<>();
/**
* Creates an empty annotation box. The box is offset from (0,0) by (17, -38).
*/
public AnnotationBox() {
this.setCache(true);
background.setX(BACKGROUND_X);
background.setY(BACKGROUND_Y);
background.setWidth(backgroundWidth);
background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * 4);
background.setArcHeight(BACKGROUND_ARC_SIZE);
background.setArcWidth(BACKGROUND_ARC_SIZE);
background.setFill(new Color(1, 1, 1, 0.75));
background.setStroke(theme);
background.setStrokeWidth(2);
background.setCache(true);
background.setCacheHint(CacheHint.SCALE);
this.getChildren().add(background);
}
/**
* Adds an annotation to the box. Use the name to reference the annotation for removal or\
* changing visibility.
* @param annotationName the name of the annotation.
* @param annotation the annotation.
*/
public void addAnnotation (String annotationName, Annotation annotation) {
annotationsByName.put(annotationName, annotation);
Platform.runLater(() -> {
this.getChildren().add(annotation.getText());
visibleAnnotations++;
update();
});
}
/**
* Adds an annotation with a constant text.
* @param annotationName The name of the annotation. Will be used to reference it later.
* @param annotationText The desired text.
*/
public void addAnnotation (String annotationName, String annotationText) {
Text text = getTextObject();
addAnnotation(annotationName, new Annotation(text, annotationText));
}
/**
* Adds an annotation with the given name. The annotation will contain the value of the given
* ObservableValue. The formatter should return a String and takes an object of the same type as
* the ObservableValue as a parameter. The String is how you want the annotation to look.
* @param annotationName The annotation name.
* @param observable The observable value the annotation will display.
* @param formatter A formatting function for the observable value.
* @param <E> The type of ObservableValue.
*/
public <E> void addAnnotation (String annotationName, ObservableValue<E> observable,
AnnotationFormatter<E> formatter) {
Text newText = getTextObject();
addAnnotation(annotationName, new Annotation<>(newText, observable, formatter));
}
/**
* Sets the visibility of the annotation with the given name if it exists.
* @param annotationName The name of the annotation
* @param visibility the desired visibility
*/
public void setAnnotationVisibility (String annotationName, boolean visibility) {
if (annotationsByName.containsKey(annotationName)) {
Text textField = annotationsByName.get(annotationName).text;
boolean currentState = textField.visibleProperty().get();
if (visibility != currentState) {
if (visibility)
visibleAnnotations++;
else
visibleAnnotations--;
}
textField.setVisible(visibility);
update();
}
}
/**
* Removes the annotation with the given name if it exits.
* @param annotationName The name given when the annotation was created.
*/
public void removeAnnotation (String annotationName) {
if (annotationName.contains(annotationName)) {
Platform.runLater(() -> {
this.getChildren().remove(annotationsByName.remove(annotationName).getText());
visibleAnnotations--;
update();
});
annotationsByName.remove(annotationName);
}
}
/**
* Moves the annotation.
* @param x x location
* @param y y location
*/
public void setLocation (double x, double y) {
Platform.runLater(()-> this.relocate(x + BACKGROUND_X, y + BACKGROUND_Y));
}
/**
* Changes the width of the annotation box. Default is 145.
* @param width new width.
*/
public void setWidth (double width) {
backgroundWidth = width;
Platform.runLater(() -> background.setWidth(backgroundWidth));
}
private void update () {
background.setVisible(visibleAnnotations != 0);
background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * visibleAnnotations);
for (int i = 1; i <= visibleAnnotations; i++) {
Text text = (Text) this.getChildren().get(i);
if (text.visibleProperty().get()) {
text.setX(X_OFFSET_TEXT);
text.setY(Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * i);
// });
}
}
}
/**
* Returns a text object for an annotation.
* @return The text object
*/
private Text getTextObject() {
Text text = new Text();
text.setFill(theme);
text.setStrokeWidth(2);
// text.setCacheHint(CacheHint.QUALITY);
text.setCache(true);
return text;
}
/**
* Set the colour of the annotation box's border and text colour.
* @param value desired colour.
*/
public void setFill (Paint value) {
theme = value;
background.setStroke(theme);
annotationsByName.forEach((name, annotation) -> annotation.getText().setFill(theme));
}
}
@@ -0,0 +1,483 @@
package seng302.visualiser.fxObjects.assets_2D;
import javafx.application.Platform;
import javafx.geometry.Point2D;
import javafx.geometry.Point3D;
import javafx.scene.AmbientLight;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.PointLight;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Polyline;
import javafx.scene.shape.Shape3D;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import seng302.visualiser.fxObjects.assets_3D.BoatMeshType;
import seng302.visualiser.fxObjects.assets_3D.BoatModel;
import seng302.visualiser.fxObjects.assets_3D.ModelFactory;
import seng302.visualiser.fxObjects.assets_3D.ModelType;
import java.util.ArrayList;
import java.util.List;
/**
* BoatGroup is a javafx group that by default contains a graphical objects for representing a 2
* dimensional boat. It contains a single polygon for the boat, a group of lines to show it's path,
* a wake object and two text labels to annotate the boat teams name and the boats velocity. The
* boat will update it's position onscreen everytime UpdatePosition is called unless the window is
* minimized in which case it attempts to store animations and apply them when the window is
* maximised.
*/
public class BoatObject extends Group {
private static final double MODEL_SCALE_FACTOR = 400;
private static final double MODEL_X_OFFSET = 0; // standard
private static final double MODEL_Y_OFFSET = 0; // standard
private static final int VIEWPORT_SIZE = 800;
private static final Color lightColor = Color.rgb(244, 255, 250);
private static final Color jewelColor = Color.rgb(0, 190, 222);
@FunctionalInterface
public interface SelectedBoatListener {
void notifySelected(BoatObject boatObject, Boolean isSelected);
}
//Constants for drawing
private static final float BOAT_HEIGHT = 15f;
private static final float BOAT_WIDTH = 10f;
private double xVelocity;
private double yVelocity;
private double lastHeading;
private double sailState;
//Graphical objects
private Polyline trail = new Polyline();
// private Polygon boatPoly;
private BoatModel boatPoly;
private Polygon sail;
private Group wake;
private Line leftLayLine;
private Line rightLayline;
private double distanceTravelled, lastRotation;
private Point2D lastPoint;
private Color colour = Color.BLACK;
private Boolean isSelected = false, destinationSet; //All boats are initialised as selected
private boolean isPlayer = false;
private Rotate rotation = new Rotate(0,0,1);
private List<SelectedBoatListener> selectedBoatListenerListeners = new ArrayList<>();
/**
* Creates a BoatGroup with the default triangular boat polygon.
*/
public BoatObject() {
this(-BOAT_WIDTH / 2, BOAT_HEIGHT / 2,
0.0, -BOAT_HEIGHT / 2,
BOAT_WIDTH / 2, BOAT_HEIGHT / 2);
}
/**
* Creates a BoatGroup with the boat being the default polygon. The head of the boat should be
* at point (0,0).
*
* @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat
* polygon.
*/
public BoatObject(double... points) {
initChildren(points);
}
/**
* Creates the javafx objects that will be the in the group by default.
*
* @param points An array of co-ordinates x1,y1,x2,y2,x3,y3... that will make up the boat
* polygon.
*/
private void initChildren(double... points) {
boatPoly = makeBoatPolygon();
boatPoly.getAssets().getTransforms().addAll(
// new Rotate(-40, new Point3D(1,0,0)),
rotation
// new Rotate(-90, new Point3D(0,0,1))
);
boatPoly.getAssets().getTransforms().add(new Scale(5, 5, 5));
// boatPoly.setDrawMode(DrawMode.FILL);
// boatPoly.setFill(colour);
// boatPoly.setFill(this.colour);
// boatPoly.setMaterial(new PhongMaterial(this.colour));
boatPoly.getAssets().setOnMouseEntered(event -> {
// boatPoly.setFill(Color.FLORALWHITE);
// boatPoly.setStroke(Color.RED);
// boatPoly.setMaterial(new PhongMaterial(Color.FLORALWHITE));
});
boatPoly.getAssets().setOnMouseExited(event -> {
// boatPoly.setMaterial(new PhongMaterial(this.colour));
// boatPoly.setFill(colour);
// boatPoly.setFill(this.colour);
// boatPoly.setStroke(Color.BLACK);
});
boatPoly.getAssets().setOnMouseClicked(event -> setIsSelected(!isSelected));
boatPoly.getAssets().setCache(true);
// boatPoly.setCacheHint(CacheHint.SPEED);
// annotationBox = new AnnotationBox();
// annotationBox.setFill(colour);
leftLayLine = new Line();
rightLayline = new Line();
trail.getStrokeDashArray().setAll(5d, 10d);
trail.setCache(true);
wake = ModelFactory.importModel(ModelType.WAKE).getAssets();
sail = new Polygon(0.0,BOAT_HEIGHT / 4,
0.0, BOAT_HEIGHT);
sailState = 0;
sail.setStrokeWidth(2.0);
sail.setStroke(Color.BLACK);
sail.setFill(Color.TRANSPARENT);
sail.setCache(true);
super.getChildren().clear();
PointLight pointLight = new PointLight(Color.WHITE);
// pointLight.setLightOn(true);
pointLight.getTransforms().add(new Translate(0, 0, -30));
super.getChildren().add(pointLight);
// pointLight = new PointLight(Color.WHITE);
//// pointLight.setLightOn(true);
// pointLight.getTransforms().add(new Translate(50, 50, -20));
// super.getChildren().add(pointLight);
// pointLight = new PointLight(Color.WHITE);
//// pointLight.setLightOn(true);
// pointLight.getTransforms().add(new Translate(50, -50, -20));
// super.getChildren().add(pointLight);
// pointLight = new PointLight(Color.WHITE);
//// pointLight.setLightOn(true);
// pointLight.getTransforms().add(new Translate(-50, -50, -20));
// super.getChildren().add(pointLight);
AmbientLight light = new AmbientLight(new Color(0.5,0.5,0.5,1));
super.getChildren().add(light);
super.getChildren().addAll(boatPoly.getAssets());
}
public void setFill (Color value) {
this.colour = value;
PhongMaterial pm = new PhongMaterial(this.colour);
for (int i=0;i<2;i++) {
Shape3D s = (Shape3D) boatPoly.getAssets().getChildren().get(i);
s.setMaterial(pm);
}
trail.setStroke(colour);
}
public BoatModel makeBoatPolygon () {
// StlMeshImporter importer = new StlMeshImporter();
// importer.read(getClass().getResource("/meshes/hollow_simple_boat.stl").toString());
// importer.read(getClass().getResource("/cube.stl").toString());
// importer.read(getClass().getResource("/meshes/simple_yacht.stl").toString());
// ObjModelImporter importer = new ObjModelImporter();
// ColModelImporter importer = new ColModelImporter();
// ColModelImporter importer = new ColModelImporter();
// importer.read(getClass().getResource("/meshes/hollow_simple_boat.dae"));
// MeshView mesh = importer.getImport()[0];
// FileInputStream inputStream = null;
// try{
// inputStream = new FileInputStream(getClass().getResource("/meshes/simple_yacht.stl").toString());
// Obj obj = ObjUtils.convertToRenderable(
// ObjReader.read(inputStream));
//
// IntBuffer indices = ObjData.getFaceVertexIndices(obj);
// FloatBuffer vertices = ObjData.getVertices(obj);
// FloatBuffer texCoords = ObjData.getTexCoords(obj);
// MeshView
// FloatBuffer normals = ObjData.getNormals(obj);
return ModelFactory.boatGameView(BoatMeshType.DINGHY, colour);
// } catch (Exception e) {
// e.printStackTrace();
// return null;
// }
}
/**
* Moves the boat and its children annotations to coordinates specified
* @param x The X coordinate to move the boat to
* @param y The Y coordinate to move the boat to
* @param rotation The rotation by which the boat moves
* @param velocity The velocity the boat is moving
* @param sailIn Boolean to toggle sail state.
* @param windDir .
*/
public void moveTo(double x, double y, double rotation, double velocity, Boolean sailIn, double windDir) {
Double dx = Math.abs(boatPoly.getAssets().getLayoutX() - x);
Double dy = Math.abs(boatPoly.getAssets().getLayoutY() - y);
Platform.runLater(() -> {
rotateTo(rotation, sailIn, windDir);
boatPoly.getAssets().setLayoutX(x);
boatPoly.getAssets().setLayoutY(y);
// if (sailIn) {
//// sail.getPoints().clear();
//// sail.getPoints().addAll(0.0, 0.0, 4.0, 1.5, 8.0, 3.0, 12.0, 3.5, 16.0, 3.0, 20.0, 1.5, 24.0, 0.0);
//// sail.getPoints().addAll(0.0, 0.0, 24.0, 0.0);
// sail.setLayoutX(x);
// sail.setLayoutY(y);
// } else {
//// animateSail();
// sail.setLayoutX(x);
// sail.setLayoutY(y);
// }
wake.setLayoutX(x);
wake.setLayoutY(y);
});
// wake.setRotation(rotation, velocity);
// rotateTo(rotation);
// boatPoly.setLayoutX(x);
// boatPoly.setLayoutY(y);
// wake.setLayoutX(x);
// wake.setLayoutY(y);
// wake.rotate(rotation);
// wake.setRotation(rotation, groundSpeed);
// isStopped = false;
// destinationSet = true;
lastRotation = rotation;
distanceTravelled += Math.sqrt((dx * dx) + (dy * dy));
if (distanceTravelled > 15 && isPlayer) {
distanceTravelled = 0d;
Platform.runLater(() -> trail.getPoints().addAll(x, y));
}
}
private Double normalizeHeading(double heading, double windDirection) {
Double normalizedHeading = heading - windDirection;
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L);
return normalizedHeading;
}
private void rotateTo(double heading, boolean sailsIn, double windDir) {
rotation.setAngle(heading);
wake.getTransforms().setAll(new Rotate(heading, new Point3D(0,0,1)));
if (!sailsIn) {
boatPoly.showSail();
Double sailWindOffset = 30.0;
Double upwindAngleLimit = 15.0;
Double downwindAngleLimit = 10.0; //Upwind from normalised horizontal
Double normalizedHeading = normalizeHeading(heading, windDir);
if (normalizedHeading < 180) {
if (normalizedHeading < sailWindOffset + upwindAngleLimit){
boatPoly.rotateSail(90 - upwindAngleLimit);
} else if (normalizedHeading > 90 + sailWindOffset){
boatPoly.rotateSail(downwindAngleLimit);
} else {
boatPoly.rotateSail(90 + sailWindOffset);
}
} else {
if (normalizedHeading > 360 - (sailWindOffset + upwindAngleLimit)){
boatPoly.rotateSail(90 + upwindAngleLimit);
} else if (normalizedHeading < 270 - sailWindOffset){
boatPoly.rotateSail(180 - downwindAngleLimit);
} else {
boatPoly.rotateSail(90 - sailWindOffset);
}
}
} else {
boatPoly.hideSail();
}
}
private void animateSail(){
Double[] points = new Double[200];
double amplitude = 2.0;
double period = 10;
for (int i = 0; i < 50; i++) {
points[i * 2] = amplitude * Math.sin(((Math.PI * i) / period + sailState));
points[i * 2 + 1] = (double) (BOAT_HEIGHT * i) / BOAT_HEIGHT / 2;
points[199 - (i * 2)] = (double) (BOAT_HEIGHT * i) / BOAT_HEIGHT / 2;
points[199 - (i * 2 + 1)] = amplitude * Math.sin(((Math.PI * i) / period + sailState));
}
if (sailState == - 2 * Math.PI) {
sailState = 0;
} else {
sailState = sailState - Math.PI / 5;
}
sail.getPoints().clear();
sail.getPoints().addAll(points);
}
public void updateLocation() {
// boatPoly.getTransforms().add(new Rotate(2, new Point3D(1,1,1)));
// double dx = xVelocity / 60;
// double dy = yVelocity / 60;
//
// distanceTravelled += Math.abs(dx) + Math.abs(dy);
// moveGroupBy(dx, dy);
//
// if (distanceTravelled > 70) {
// distanceTravelled = 0d;
//
// if (lastPoint != null) {
// Line l = new Line(
// lastPoint.getX(),
// lastPoint.getY(),
// boatPoly.getLayoutX(),
// boatPoly.getLayoutY()
// );
// l.getStrokeDashArray().setAll(3d, 7d);
// l.setStroke(colour);
// l.setCache(true);
// l.setCacheHint(CacheHint.SPEED);
// lineGroup.getChildren().add(l);
// }
// lastPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY());
// }
// wake.updatePosition();
}
// /**
// * This function works out if a boat is going upwind or down wind. It looks at the boats current position, the next
// * gates position and the current wind
// * If bot the wind vector from the next gate and the boat from the next gate lay on the same side, then the boat is
// * going up wind, if they are on different sides of the gate, then the boat is going downwind
// * @param canvasController
// */
// public Boolean isUpwindLeg(GameViewController canvasController, Mark nextMark) {
//
// Double windAngle = StreamParser.getWindDirection();
// GateMark thisGateMark = (GateMark) nextMark;
// SingleMark nextMark1 = thisGateMark.getSingleMark1();
// SingleMark nextMark2 = thisGateMark.getSingleMark2();
// Point2D nextMarkPoint1 = canvasController.findScaledXY(nextMark1.getLatitude(), nextMark1.getLongitude());
// Point2D nextMarkPoint2 = canvasController.findScaledXY(nextMark2.getLatitude(), nextMark2.getLongitude());
//
// Point2D boatCurrentPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY());
// Point2D windTestPoint = GeoUtility.makeArbitraryVectorPoint(nextMarkPoint1, windAngle, 10d);
//
//
// Integer boatLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, boatCurrentPoint);
// Integer windLineFuncResult = GeoUtility.lineFunction(nextMarkPoint1, nextMarkPoint2, windTestPoint);
//
//
// /*
// If both the wind vector from the gate and the boat from the gate are on the same side of that gate, then the
// boat is travelling into the wind. thus upwind. Otherwise if they are on different sides, then the boat is going
// with the wind.
// */
// return boatLineFuncResult.equals(windLineFuncResult);
// return true;
// }
public void setIsSelected(Boolean isSelected) {
updateListener(isSelected);
this.isSelected = isSelected;
setLineGroupVisible(isSelected);
setWakeVisible(isSelected);
setLayLinesVisible(isSelected);
}
public void setVisibility (boolean teamName, boolean velocity, boolean estTime, boolean legTime,
boolean trail, boolean wake) {
// boatAnnotations.setVisible(teamName, velocity, estTime, legTime);
// this.wake.setVisible(wake);
this.trail.setVisible(trail);
}
public void setLineGroupVisible(Boolean visible) {
trail.setVisible(visible);
}
public void setWakeVisible(Boolean visible) {
// wake.setVisible(visible);
}
public void setLayLinesVisible(Boolean visible) {
leftLayLine.setVisible(visible);
rightLayline.setVisible(visible);
}
public void setLaylines(Line line1, Line line2) {
this.leftLayLine = line1;
this.rightLayline = line2;
}
public ArrayList<Line> getLaylines() {
ArrayList<Line> laylines = new ArrayList<>();
laylines.add(leftLayLine);
laylines.add(rightLayline);
return laylines;
}
public Group getWake () {
return wake;
}
public Node getTrail() {
return trail;
}
public Double getBoatLayoutX() {
return boatPoly.getAssets().getLayoutX();
}
public Double getBoatLayoutY() {
return boatPoly.getAssets().getLayoutY();
}
/**
* Sets this boat to appear highlighted
*/
public void setAsPlayer() {
// boatPoly.getPoints().setAll(
// -BOAT_WIDTH / 1.75, BOAT_HEIGHT / 1.75,
// 0.0, -BOAT_HEIGHT / 1.75,
// BOAT_WIDTH / 1.75, BOAT_HEIGHT / 1.75
// );
// boatPoly.setStroke(Color.BLACK);
// boatPoly.setStrokeWidth(2);
// boatPoly.setStrokeLineCap(StrokeLineCap.ROUND);
isPlayer = true;
animateSail();
}
public void setTrajectory(double heading, double velocity, double windDir) {
// wake.r(lastHeading - heading, velocity);
// rotateTo(heading, false, windDir);
xVelocity = Math.cos(Math.toRadians(heading)) * velocity;
yVelocity = Math.sin(Math.toRadians(heading)) * velocity;
lastHeading = heading;
}
public Boolean getSelected() {
return isSelected;
}
public void setTrajectory(double heading, double velocity, double scaleFactorX, double scaleFactorY) {
// wake.setRotation(lastHeading - heading, velocity);
// rotateTo(heading);
// xVelocity = Math.cos(Math.toRadians(heading)) * velocity * scaleFactorX;
// yVelocity = Math.sin(Math.toRadians(heading)) * velocity * scaleFactorY;
lastHeading = heading;
}
private void updateListener(Boolean isSelected) {
for (SelectedBoatListener sbl : selectedBoatListenerListeners) {
sbl.notifySelected(this, isSelected);
}
}
public void addSelectedBoatListener(SelectedBoatListener sbl) {
selectedBoatListenerListeners.add(sbl);
}
}
@@ -0,0 +1,15 @@
package seng302.visualiser.fxObjects.assets_2D;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
/**
* Polygon with default course border settings.
*/
public class CourseBoundary extends Polygon {
public CourseBoundary() {
this.setStroke(new Color(0.0f, 0.0f, 0.74509807f, 1));
this.setStrokeWidth(3);
this.setFill(new Color(0,0,0,0));
}
}
@@ -0,0 +1,20 @@
package seng302.visualiser.fxObjects.assets_2D;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Line;
/**
* Visual object representing a gate, intended to connect two mark objects.
*/
public class Gate extends Line {
public Gate () {
super.setStrokeWidth(2);
super.getStrokeDashArray().setAll(2d, 5d);
}
public Gate (Paint colour) {
this();
super.setStroke(colour);
}
}
@@ -0,0 +1,147 @@
package seng302.visualiser.fxObjects.assets_2D;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.*;
import javafx.scene.transform.Rotate;
// TODO: 16/08/17 this class used to be well written... FeelsBadMan. Maybe lose the ternary operators.
/**
* Factory class for making rounding arrows for mark objects out of JavaFX objects.
*/
public class MarkArrowFactory {
/**
* The side of the boat that will be closest to the mark.
*/
public enum RoundingSide {
PORT,
STARBOARD,
}
public static final double MARK_ARROW_SEPARATION = 15;
public static final double ARROW_LENGTH = 75;
public static final double ARROW_HEAD_DEPTH = 10;
public static final double ARROW_HEAD_WIDTH = 6;
public static final double STROKE_WIDTH = 3;
/**
* Creates an entry arrow group showing an arrow into and out of the rounding area. It is centered on (0, 0).
* @param roundingSide The side of the boat that will be closest to the mark.
* @param angleOfEntry The angle between this mark and the last one as a heading from north in degrees.
* @param angleOfExit The angle between this mark and the next one as a heading from north in degrees.
* @param colour The desired colour of the arrows.
* @return The group containing all JavaFX objects.
*/
public static Group constructEntryArrow (RoundingSide roundingSide, double angleOfEntry,
double angleOfExit, Paint colour) {
if (roundingSide == RoundingSide.PORT && angleOfEntry < angleOfExit && Math.abs(angleOfExit - angleOfEntry) < 180) {
return makeInteriorAngle(roundingSide, angleOfExit, angleOfEntry, colour);
} else if (roundingSide == RoundingSide.STARBOARD && angleOfEntry > angleOfExit && -Math.abs(angleOfEntry - angleOfExit) > -180) {
return makeInteriorAngle(roundingSide, angleOfExit, angleOfEntry, colour);
}
angleOfEntry = 180 - angleOfEntry;
Group arrow = new Group();
Group exitSection = constructExitArrow(roundingSide, angleOfExit, colour);
angleOfExit = 180 - angleOfExit;
Arc roundSection = new Arc(
0, 0, MARK_ARROW_SEPARATION, MARK_ARROW_SEPARATION,
(roundingSide == RoundingSide.PORT ? -180 : 0) + angleOfEntry,
roundingSide == RoundingSide.PORT ? Math.abs(angleOfExit - angleOfEntry) : -Math.abs(angleOfEntry - angleOfExit)
);
roundSection.setStrokeWidth(STROKE_WIDTH);
roundSection.setType(ArcType.OPEN);
roundSection.setStroke(colour);
roundSection.setFill(new Color(0,0,0,0));
Polygon entrySection = constructLineSegment(
roundingSide == RoundingSide.PORT ? RoundingSide.STARBOARD : RoundingSide.PORT, 180 + angleOfEntry, colour
);
arrow.getChildren().addAll(exitSection, roundSection, entrySection);
return arrow;
}
private static Group makeInteriorAngle (RoundingSide roundingSide, double angleOfExit, double angleOfEntry, Paint colour) {
Group arrow = new Group();
Polygon lineSegment;
angleOfEntry = Math.toRadians(360 - angleOfEntry);
angleOfExit = Math.toRadians(180 - angleOfExit);
int multiplier = roundingSide == RoundingSide.STARBOARD ? -1 : 1;
double xStart = multiplier * MARK_ARROW_SEPARATION * Math.sin(angleOfEntry + Math.PI / 2);
double yStart = multiplier * MARK_ARROW_SEPARATION * Math.cos(angleOfEntry + Math.PI / 2);
xStart = xStart + (ARROW_LENGTH * Math.sin(angleOfEntry));
yStart = yStart + (ARROW_LENGTH * Math.cos(angleOfEntry));
multiplier = roundingSide == RoundingSide.STARBOARD ? 1 : -1;
double xEnd = multiplier * MARK_ARROW_SEPARATION * Math.sin(angleOfExit + Math.PI / 2);
double yEnd = multiplier * MARK_ARROW_SEPARATION * Math.cos(angleOfExit + Math.PI / 2);
xEnd = xEnd + (ARROW_LENGTH * Math.sin(angleOfExit));
yEnd = yEnd + (ARROW_LENGTH * Math.cos(angleOfExit));
lineSegment = new Polygon(
xStart, yStart,
xEnd, yEnd
);
lineSegment.setStroke(colour);
lineSegment.setFill(Color.BLUE);
lineSegment.setStrokeWidth(STROKE_WIDTH);
lineSegment.setStrokeLineCap(StrokeLineCap.ROUND);
Polyline arrowHead = constructArrowHead(
90 + Math.toDegrees(Math.atan2(yStart - yEnd, xEnd - xStart)),
colour
);
arrowHead.setLayoutX(xEnd);
arrowHead.setLayoutY(yEnd);
arrow.getChildren().addAll(lineSegment, arrowHead);
return arrow;
}
/**
* Creates an exit arrow group pointing towards the next mark.
* @param roundingSide The side of the boat that will be closest to the mark.
* @param angle The angle to the next mark as a heading from north in degrees.
* @param colour The colour of the arrow.
* @return The group containing all the JavaFX objects.
*/
public static Group constructExitArrow (RoundingSide roundingSide, double angle, Paint colour) {
angle = 180 - angle;
Group arrow = new Group();
Polygon arrowBody = constructLineSegment(roundingSide, angle, colour);
Polyline arrowHead = constructArrowHead(angle, colour);
arrowHead.setLayoutX(arrowBody.getPoints().get(2));
arrowHead.setLayoutY(arrowBody.getPoints().get(3));
arrow.getChildren().addAll(arrowBody, arrowHead);
return arrow;
}
private static Polygon constructLineSegment (RoundingSide roundingSide, double angle, Paint colour) {
Polygon lineSegment;
angle = Math.toRadians(angle);
int multiplier = roundingSide == RoundingSide.STARBOARD ? 1 : -1;
double xStart = multiplier * MARK_ARROW_SEPARATION * Math.sin(angle + Math.PI / 2);
double yStart = multiplier * MARK_ARROW_SEPARATION * Math.cos(angle + Math.PI / 2);
double xEnd = xStart + (ARROW_LENGTH * Math.sin(angle));
double yEnd = yStart + (ARROW_LENGTH * Math.cos(angle));
lineSegment = new Polygon(
xStart, yStart,
xEnd, yEnd
);
lineSegment.setStroke(colour);
lineSegment.setFill(Color.BLUE);
lineSegment.setStrokeWidth(STROKE_WIDTH);
lineSegment.setStrokeLineCap(StrokeLineCap.ROUND);
return lineSegment;
}
private static Polyline constructArrowHead (double rotation, Paint colour) {
Polyline arrow = new Polyline(
-ARROW_HEAD_WIDTH, -ARROW_HEAD_DEPTH,
0, 0,
ARROW_HEAD_WIDTH, -ARROW_HEAD_DEPTH
);
arrow.getTransforms().add(new Rotate(-rotation));
arrow.setStrokeLineCap(StrokeLineCap.ROUND);
arrow.setStroke(colour);
arrow.setStrokeWidth(STROKE_WIDTH);
return arrow;
}
}
@@ -0,0 +1,100 @@
package seng302.visualiser.fxObjects.assets_2D;
import java.util.ArrayList;
import java.util.List;
import javafx.application.Platform;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.transform.Scale;
import seng302.visualiser.fxObjects.assets_3D.ModelFactory;
import seng302.visualiser.fxObjects.assets_3D.ModelType;
/**
* Visual object for a mark. Contains a coloured circle and any specified arrows.
*/
public class Marker extends Group {
private Group mark = ModelFactory.importModel(ModelType.PLAIN_MARKER).getAssets();
private Paint colour = Color.BLACK;
private List<Group> enterArrows = new ArrayList<>();
private List<Group> exitArrows = new ArrayList<>();
private int enterArrowIndex = 0;
private int exitArrowIndex = 0;
/**
* Creates a new Marker containing only a circle. The default colour is black.
*/
public Marker() {
// mark.setRadius(5);
// mark.setCenterX(0);
// mark.setCenterY(0);
Platform.runLater(() -> {
mark.getTransforms().add(new Scale(5,5,5));
this.getChildren().addAll(mark, new Group());
}); //Empty group placeholder or arrows.
}
/**
* Creates a new Marker containing only a circle of the given colour.
* @param colour the desired colour for the marker.
*/
public Marker(Paint colour) {
this();
this.colour = colour;
// mark.setFill(colour);
}
/**
* Adds an exit and entry arrow pair to the mark. Arrows are hidden and shown in the order they
* are created by calling showNextEnterArrow() or showNextExitArrow()
* @param roundingSide the side the marker will be from the perspective of the arrow.
* @param entryAngle The angle the arrow will point towards a marker
* @param exitAngle The angle the arrow wil point from the marker.
*/
public void addArrows(MarkArrowFactory.RoundingSide roundingSide, double entryAngle,
double exitAngle) {
//Change Color.GRAY to this.colour to revert all gray arrows.
enterArrows.add(
MarkArrowFactory.constructEntryArrow(roundingSide, entryAngle, exitAngle, Color.GRAY)
);
exitArrows.add(
MarkArrowFactory.constructExitArrow(roundingSide, exitAngle, Color.GRAY)
);
}
/**
* Shows the next EnterArrow. Does nothing if there are no more enter arrows. Other arrows become hidden.
*/
public void showNextEnterArrow() {
showArrow(enterArrows, enterArrowIndex);
enterArrowIndex++;
}
/**
* Shows the next ExitArrow. Does nothing if there are no more enter arrows. Other arrows become hidden.
*/
public void showNextExitArrow() {
showArrow(exitArrows, exitArrowIndex);
exitArrowIndex++;
}
private void showArrow(List<Group> arrowList, int arrowListIndex) {
if (arrowListIndex < arrowList.size()) {
if (arrowListIndex == 1) {;
}
Platform.runLater(() -> {
this.getChildren().remove(1);
this.getChildren().add(arrowList.get(arrowListIndex));
});
}
}
/**
* Hides all arrows.
*/
public void hideAllArrows() {
Platform.runLater(() -> this.getChildren().setAll(mark, new Group()));
}
}
@@ -0,0 +1,124 @@
package seng302.visualiser.fxObjects.assets_2D;
import javafx.application.Platform;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.transform.Rotate;
/**
* A group containing objects used to represent wakes onscreen. Contains functionality for their animation.
*/
public class Wake extends Group {
//The number of wakes
private int numWakes = 8;
//The total possible difference between the first wake and the last. Increasing/Decreasing this will make wakes fan out more/less.
private final double MAX_DIFF = 75;
//Increasing/decreasing this will alter the speed that wakes converge when the heading stop changing. Anything over about 1500 may cause oscillation.
private final int UNIFICATION_SPEED = 45;
private Arc[] arcs = new Arc[numWakes];
private double[] rotationalVelocities = new double[numWakes];
private double[] rotations = new double[numWakes];
/**
* Create a wake at the given location.
*
* @param startingX x location where the tip of wake arcs will be.
* @param startingY y location where the tip of wake arcs will be.
*/
Wake(double startingX, double startingY) {
super.setLayoutX(startingX);
super.setLayoutY(startingY);
Arc arc;
for (int i = 0; i < numWakes; i++) {
//Default triangle is -110 deg out of phase with a default wake and has angle of 40 deg.
arc = new Arc(0, 0, 0, 0, -110, 40);
arc.setCache(true);
arc.setCacheHint(CacheHint.ROTATE);
arc.setType(ArcType.OPEN);
arc.setStroke(
new Color(
0.18, 0.7, 1.0, 1.0 + (-0.99 / numWakes * i)
)
);
arc.setStrokeWidth(3.0);
arc.setStrokeLineCap(StrokeLineCap.ROUND);
arc.setFill(new Color(0.0, 0.0, 0.0, 0.0));
arcs[i] = arc;
arc.getTransforms().setAll(
new Rotate(1)
);
}
super.getChildren().addAll(arcs);
}
void setRotation (double rotation, double velocity) {
// if (Math.abs(rotations[0] - rotation) > 20) {
Platform.runLater(() -> {
rotate(rotation);
double rad = (14 / numWakes) + velocity;
for (Arc arc : arcs) {
arc.setRadiusX(rad);
arc.setRadiusY(rad);
rad += (14 / numWakes) + (velocity / 2.5);
}
});
// } else {
// rotations[0] = rotation;
// ((Rotate) arcs[0].getTransforms().get(0)).setAngle(rotation);
// for (int i = 1; i < numWakes; i++) {
// double wakeSeparationRad = Math.toRadians(rotations[i - 1] - rotations[i]);
// double shortestDistance = Math.atan2(
// Math.sin(wakeSeparationRad),
// Math.cos(wakeSeparationRad)
// );
// double distDeg = Math.toDegrees(shortestDistance);
// if (rotationalVelocities[i - 1] < 0.01 && rotationalVelocities[i - 1] > -0.01) {
// rotationalVelocities[i] = distDeg / UNIFICATION_SPEED * 2 * Math.log(Math.abs(distDeg) + 1) / Math.log(MAX_DIFF / numWakes);
//
// } else {
// if (distDeg < (MAX_DIFF / numWakes)) {
// rotationalVelocities[i] = distDeg / UNIFICATION_SPEED * Math.log(Math.abs(distDeg) + 1) / Math.log(MAX_DIFF / numWakes);
// } else
// rotationalVelocities[i] = rotationalVelocities[i - 1];
// }
// }
// }
// double rad = (14 / numWakes) + velocity;
// for (Arc arc : arcs) {
// arc.setRadiusX(rad);
// arc.setRadiusY(rad);
// rad += (14 / numWakes) + (velocity / 2.5);
// }
}
/**
* Arcs rotate based on the distance they would have travelled over the supplied time interval.
*/
void updatePosition() {
for (int i = 0; i < numWakes; i++) {
rotations[i] = rotations[i] + rotationalVelocities[i];
((Rotate) arcs[i].getTransforms().get(0)).setAngle(rotations[i]);
}
}
/**
* Rotate all wakes to the given rotation.
*
* @param rotation the from north angle in degrees to rotate to.
*/
void rotate(double rotation) {
for (int i = 0; i < arcs.length; i++) {
rotations[i] = rotation;
rotationalVelocities[i] = 0;
arcs[i].getTransforms().setAll(new Rotate(rotation));
}
}
}
@@ -0,0 +1,25 @@
package seng302.visualiser.fxObjects.assets_2D;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Polyline;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
/**
* Created by cir27 on 5/09/17.
*/
public class WindArrow extends Polyline {
public WindArrow(Paint fill) {
this.getPoints().addAll(
-10d, 15d,
0d, 25d,
0d, -25d,
0d, 25d,
10d, 15d
);
this.setStrokeLineCap(StrokeLineCap.ROUND);
this.setStroke(fill);
this.setStrokeWidth(5);
this.setStrokeLineJoin(StrokeLineJoin.ROUND);
}
}