Merge remote-tracking branch 'origin/develop' into Story80_BoatCustomization

# Conflicts:
#	src/main/java/seng302/gameServer/GameState.java
#	src/main/java/seng302/gameServer/ServerPacketParser.java
#	src/main/java/seng302/gameServer/ServerToClientThread.java
#	src/main/java/seng302/gameServer/messages/ChatterMessage.java
#	src/main/java/seng302/gameServer/messages/MarkRoundingMessage.java
#	src/main/java/seng302/model/ServerYacht.java
#	src/main/java/seng302/visualiser/ClientToServerThread.java
#	src/main/java/seng302/visualiser/GameClient.java
#	src/main/java/seng302/visualiser/GameView.java
This commit is contained in:
Alistair McIntyre
2017-08-17 13:31:27 +12:00
26 changed files with 1020 additions and 376 deletions
@@ -15,6 +15,8 @@ import javafx.scene.layout.Pane;
import seng302.gameServer.GameState;
import seng302.gameServer.MainServerThread;
import seng302.gameServer.messages.BoatAction;
import seng302.gameServer.messages.BoatStatus;
import seng302.gameServer.messages.BoatAction;
import seng302.model.ClientYacht;
import seng302.model.RaceState;
import seng302.model.stream.packets.StreamPacket;
@@ -27,6 +29,7 @@ import seng302.model.stream.xml.parser.RaceXMLData;
import seng302.model.stream.xml.parser.RegattaXMLData;
import seng302.utilities.StreamParser;
import seng302.utilities.XMLParser;
import seng302.visualiser.controllers.FinishScreenViewController;
import seng302.visualiser.controllers.LobbyController;
import seng302.visualiser.controllers.LobbyController.CloseStatus;
import seng302.visualiser.controllers.RaceViewController;
@@ -169,17 +172,7 @@ public class GameClient {
}
private void loadRaceView() {
FXMLLoader fxmlLoader = new FXMLLoader(
RaceViewController.class.getResource("/views/RaceView.fxml"));
try {
final Node node = fxmlLoader.load();
Platform.runLater(() -> {
holderPane.getChildren().clear();
holderPane.getChildren().add(node);
});
} catch (IOException e) {
e.printStackTrace();
}
FXMLLoader fxmlLoader = loadFXMLToHolder("/views/RaceView.fxml");
holderPane.getScene().setOnKeyPressed(this::keyPressed);
holderPane.getScene().setOnKeyReleased(this::keyReleased);
raceView = fxmlLoader.getController();
@@ -188,17 +181,25 @@ public class GameClient {
}
private void loadFinishScreenView() {
FXMLLoader fxmlLoader = loadFXMLToHolder("/views/FinishScreenView.fxml");
FinishScreenViewController controller = fxmlLoader.getController();
controller.setFinishers(raceState.getPlayerPositions());
}
private FXMLLoader loadFXMLToHolder(String fxmlLocation) {
FXMLLoader fxmlLoader = new FXMLLoader(
getClass().getResource("/views/FinishScreenView.fxml"));
getClass().getResource(fxmlLocation)
);
try {
final Node finishScreenFX = fxmlLoader.load();
final Node fxmlLoaderFX = fxmlLoader.load();
Platform.runLater(() -> {
holderPane.getChildren().clear();
holderPane.getChildren().add(finishScreenFX);
holderPane.getChildren().add(fxmlLoaderFX);
});
} catch (IOException e) {
e.printStackTrace();
}
return fxmlLoader;
}
private void parsePackets() {
@@ -243,6 +244,7 @@ public class GameClient {
allBoatsMap.forEach((id, boat) ->
clientLobbyList.add(boat.getBoatName())
);
raceState.setBoats(allBoatsMap.values());
break;
case RACE_START_STATUS:
@@ -281,8 +283,8 @@ public class GameClient {
private void updatePosition(PositionUpdateData positionData) {
if (positionData.getType() == DeviceType.YACHT_TYPE) {
if (allXMLReceived() && allBoatsMap.containsKey(positionData.getDeviceId())) {
ClientYacht clientYacht = allBoatsMap.get(positionData.getDeviceId());
clientYacht.updateLocation(positionData.getLat(),
ClientYacht yacht = allBoatsMap.get(positionData.getDeviceId());
yacht.updateLocation(positionData.getLat(),
positionData.getLon(), positionData.getHeading(),
positionData.getGroundSpeed());
}
@@ -300,13 +302,10 @@ public class GameClient {
private void updateMarkRounding(MarkRoundingData roundingData) {
if (allXMLReceived()) {
ClientYacht clientYacht = allBoatsMap.get(roundingData.getBoatId());
clientYacht.setMarkRoundingTime(roundingData.getTimeStamp());
clientYacht.updateTimeSinceLastMarkProperty(
raceState.getRaceTime() - roundingData.getTimeStamp());
clientYacht.setLastMarkRounded(
courseData.getCompoundMarks().get(
roundingData.getMarkId()
)
clientYacht.roundMark(
courseData.getCompoundMarks().get(roundingData.getMarkId()),
roundingData.getTimeStamp(),
raceState.getRaceTime() - roundingData.getTimeStamp()
);
}
}
@@ -314,37 +313,36 @@ public class GameClient {
private void processRaceStatusUpdate(RaceStatusData data) {
if (allXMLReceived()) {
raceState.updateState(data);
if (raceView != null) {
raceView.getGameView().setWindDir(raceState.getWindDirection());
}
boolean raceFinished = true;
for (ClientYacht yacht : allBoatsMap.values()) {
if (yacht.getBoatStatus() != 3) {
if (yacht.getBoatStatus() != BoatStatus.FINISHED.getCode()) {
raceFinished = false;
}
}
if (raceFinished == true) {
close();
loadFinishScreenView();
}
for (long[] boatData : data.getBoatData()) {
ClientYacht clientYacht = allBoatsMap.get((int) boatData[0]);
clientYacht.setEstimateTimeTillNextMark(raceState.getRaceTime() - boatData[1]);
clientYacht.setEstimateTimeAtFinish(boatData[2]);
int legNumber = (int) boatData[3];
clientYacht.setLegNumber(legNumber);
clientYacht.setBoatStatus((int) boatData[4]);
if (legNumber != clientYacht.getLegNumber()) {
int placing = 1;
for (ClientYacht otherClientYacht : allBoatsMap.values()) {
if (otherClientYacht.getSourceId() != boatData[0] &&
clientYacht.getLegNumber() <= otherClientYacht.getLegNumber())
placing++;
}
clientYacht.setPositionInteger(placing);
clientYacht.setLegNumber(legNumber);
updatePlayerPositions();
}
}
if (raceFinished) {
close();
loadFinishScreenView();
}
}
}
private void updatePlayerPositions() {
raceState.sortPlayers();
for (ClientYacht yacht : raceState.getPlayerPositions()) {
yacht.setPosition(raceState.getPlayerPositions().indexOf(yacht) + 1);
}
}
@@ -367,22 +365,16 @@ public class GameClient {
socketThread.sendBoatAction(BoatAction.DOWNWIND); break;
case ENTER: // tack/gybe
socketThread.sendBoatAction(BoatAction.TACK_GYBE); break;
//TODO Allow a zoom in and zoom out methods
case Z: // zoom in
System.out.println("Zoom in");
break;
case X: // zoom out
System.out.println("Zoom out");
break;
}
}
private void keyReleased(KeyEvent e) {
switch (e.getCode()) {
//TODO 12/07/17 Determine the sail state and send the appropriate packet (eg. if sails are in, send a sail out packet)
case SHIFT: // sails in/sails out
socketThread.sendBoatAction(BoatAction.SAILS_IN);
raceView.getGameView().getPlayerYacht().toggleSail();
allBoatsMap.get(socketThread.getClientId()).toggleSail();
break;
case PAGE_UP:
case PAGE_DOWN:
@@ -400,7 +392,11 @@ public class GameClient {
private void showCollisionAlert(YachtEventData yachtEventData) {
// 33 is the agreed code to show collision
if (yachtEventData.getEventId() == 33) {
raceView.showCollision(yachtEventData.getSubjectId());
raceState.storeCollision(
allBoatsMap.get(
yachtEventData.getSubjectId().intValue()
)
);
}
}
}
+219 -65
View File
@@ -16,6 +16,8 @@ import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
@@ -25,6 +27,9 @@ import javafx.scene.shape.Polygon;
import javafx.scene.text.Text;
import javafx.util.Duration;
import seng302.model.ClientYacht;
import seng302.gameServer.messages.RoundingSide;
import seng302.model.ClientYacht;
import seng302.model.Colors;
import seng302.model.GeoPoint;
import seng302.model.Limit;
import seng302.model.mark.CompoundMark;
@@ -35,6 +40,7 @@ import seng302.visualiser.fxObjects.AnnotationBox;
import seng302.visualiser.fxObjects.BoatObject;
import seng302.visualiser.fxObjects.CourseBoundary;
import seng302.visualiser.fxObjects.Gate;
import seng302.visualiser.fxObjects.MarkArrowFactory;
import seng302.visualiser.fxObjects.Marker;
import seng302.visualiser.map.Boundary;
import seng302.visualiser.map.CanvasMap;
@@ -70,11 +76,13 @@ public class GameView extends Pane {
private Map<ClientYacht, BoatObject> boatObjects = new HashMap<>();
private Map<ClientYacht, AnnotationBox> annotations = new HashMap<>();
private ObservableList<Node> gameObjects;
private BoatObject selectedBoat = null;
private Group annotationsGroup = new Group();
private Group wakesGroup = new Group();
private Group boatObjectGroup = new Group();
private Group trails = new Group();
private Group markers = new Group();
private List<CompoundMark> course = new ArrayList<>();
private ImageView mapImage = new ImageView();
@@ -91,19 +99,19 @@ public class GameView extends Pane {
double scaleFactor = 1;
public void zoomOut() {
scaleFactor = 0.95;
for (Node child : getChildren()) {
child.setScaleX(child.getScaleX() * scaleFactor);
child.setScaleY(child.getScaleY() * scaleFactor);
private void zoomOut() {
scaleFactor = 0.1;
if (this.getScaleX() > 0.5) {
this.setScaleX(this.getScaleX() - scaleFactor);
this.setScaleY(this.getScaleY() - scaleFactor);
}
}
public void zoomIn() {
scaleFactor = 1.05;
for (Node child : getChildren()) {
child.setScaleX(child.getScaleX() * scaleFactor);
child.setScaleY(child.getScaleY() * scaleFactor);
private void zoomIn() {
scaleFactor = 0.10;
if (this.getScaleX() < 2.5) {
this.setScaleX(this.getScaleX() + scaleFactor);
this.setScaleY(this.getScaleY() + scaleFactor);
}
}
@@ -112,14 +120,25 @@ public class GameView extends Pane {
VERTICAL
}
public GameView() {
private void trackBoat() {
if (selectedBoat != null) {
double x = selectedBoat.getBoatLayoutX();
double y = selectedBoat.getBoatLayoutY();
double displacementX = this.getWidth();
double displacementY = this.getHeight();
this.setLayoutX((-x + (displacementX / 2.0)) * this.getScaleX());
this.setLayoutY((-y + (displacementY / 2.0)) * this.getScaleY());
} else {
this.setLayoutX(0);
this.setLayoutY(0);
}
}
public GameView () {
gameObjects = this.getChildren();
// create image view for map, bind panel size to image
gameObjects.add(mapImage);
fpsDisplay.setLayoutX(5);
fpsDisplay.setLayoutY(20);
fpsDisplay.setStrokeWidth(2);
gameObjects.add(fpsDisplay);
gameObjects.add(raceBorder);
gameObjects.add(markers);
initializeTimer();
@@ -137,6 +156,7 @@ public class GameView extends Pane {
@Override
public void handle(long now) {
trackBoat();
if (lastTime == 0) {
lastTime = now;
} else {
@@ -160,9 +180,7 @@ public class GameView extends Pane {
lastTime = now;
}
}
// Platform.runLater(() ->
boatObjects.forEach((boat, boatObject) -> boatObject.updateLocation());
// );
boatObjects.forEach((boat, boatObject) -> boatObject.updateLocation());
}
};
}
@@ -204,6 +222,7 @@ public class GameView extends Pane {
mapImage.fitHeightProperty().bind(((AnchorPane) this.getParent()).heightProperty());
}
// TODO: 16/08/17 Break up this function
/**
* Adds a course to the GameView. The view is scaled accordingly unless a border is set in which
* case the course is added relative ot the border.
@@ -213,6 +232,23 @@ public class GameView extends Pane {
*/
public void updateCourse(List<CompoundMark> newCourse, List<Corner> sequence) {
markerObjects = new HashMap<>();
for (Corner corner : sequence) { //Makes course out of all compound marks.
for (CompoundMark compoundMark : newCourse) {
if (corner.getCompoundMarkID() == compoundMark.getId()) {
course.add(compoundMark);
}
}
}
// TODO: 16/08/17 Updating mark roundings here. It should not happen here. Nor should it be done this way.
for (Corner corner : sequence){
CompoundMark compoundMark = course.get(corner.getSeqID() - 1);
compoundMark.setRoundingSide(
RoundingSide.getRoundingSide(corner.getRounding())
);
}
final List<Gate> gates = new ArrayList<>();
Paint colour = Color.BLACK;
//Creates new markers
@@ -227,16 +263,6 @@ public class GameView extends Pane {
for (Mark mark : cMark.getMarks()) {
makeAndBindMarker(mark, colour);
}
//UNCOMMENT THIS TO HIGHLIGHT SUBMARKS 1 and 2 RED AND GREEN RESPECTIVELY FOR DEBUG
//(instead of above for loop)
// for (Mark mark : cMark.getMarks()) {
// if (mark.getSeqID() == 1) {
// makeAndBindMarker(mark, Color.RED);
// } else {
// makeAndBindMarker(mark, Color.GREEN);
// }
// }
//Create gate line
if (cMark.isGate()) {
for (int i = 1; i < cMark.getMarks().size(); i++) {
@@ -251,6 +277,73 @@ public class GameView extends Pane {
}
colour = Color.BLACK;
}
//Creating mark arrows.
for (int i=1; i < sequence.size()-1; i++) { //General case.
double averageLat = 0;
double averageLng = 0;
int numMarks = 0;
for (Mark mark : course.get(i-1).getMarks()) {
numMarks += 1;
averageLat += mark.getLat();
averageLng += mark.getLng();
}
GeoPoint lastMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks);
numMarks = 0;
averageLat = 0;
averageLng = 0;
for (Mark mark : course.get(i+1).getMarks()) {
numMarks += 1;
averageLat += mark.getLat();
averageLng += mark.getLng();
}
GeoPoint nextMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks);
// TODO: 16/08/17 This comparison is cancer and deserves to die.
for (Mark mark : course.get(i).getMarks()) {
markerObjects.get(mark).addArrows(
mark.getRoundingSide() == RoundingSide.STARBOARD ? MarkArrowFactory.RoundingSide.STARBOARD : MarkArrowFactory.RoundingSide.PORT,
GeoUtility.getBearing(lastMarkAv, mark),
GeoUtility.getBearing(mark, nextMarkAv)
);
}
}
// TODO: 16/08/17 Make this cleaner
//First mark case
double averageLat = 0;
double averageLng = 0;
int numMarks = 0;
for (Mark mark : course.get(1).getMarks()) {
numMarks += 1;
averageLat += mark.getLat();
averageLng += mark.getLng();
}
GeoPoint firstMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks);
for (Mark mark : course.get(0).getMarks()) {
markerObjects.get(mark).addArrows(
mark.getRoundingSide() == RoundingSide.STARBOARD ? MarkArrowFactory.RoundingSide.STARBOARD : MarkArrowFactory.RoundingSide.PORT,
0d, //90
GeoUtility.getBearing(mark, firstMarkAv)
);
}
//Last Mark case
numMarks = 0;
averageLat = 0;
averageLng = 0;
for (Mark mark : course.get(course.size()-2).getMarks()) {
numMarks += 1;
averageLat += mark.getLat();
averageLng += mark.getLng();
}
GeoPoint secondToLastMarkAv = new GeoPoint(averageLat / numMarks, averageLng / numMarks);
for (Mark mark : course.get(course.size()-1).getMarks()) {
markerObjects.get(mark).addArrows(
mark.getRoundingSide() == RoundingSide.STARBOARD ? MarkArrowFactory.RoundingSide.STARBOARD : MarkArrowFactory.RoundingSide.PORT,
GeoUtility.getBearing(secondToLastMarkAv, mark),
GeoUtility.getBearing(mark, mark)
);
}
//Scale race to markers if there is no border.
if (borderPoints == null) {
rescaleRace(new ArrayList<>(markerObjects.keySet()));
@@ -258,8 +351,8 @@ public class GameView extends Pane {
//Move the Markers to initial position.
markerObjects.forEach(((mark, marker) -> {
Point2D p2d = findScaledXY(mark.getLat(), mark.getLng());
marker.setCenterX(p2d.getX());
marker.setCenterY(p2d.getY());
marker.setLayoutX(p2d.getX());
marker.setLayoutY(p2d.getY());
}));
Platform.runLater(() -> {
markers.getChildren().clear();
@@ -276,11 +369,12 @@ public class GameView extends Pane {
*/
private void makeAndBindMarker(Mark observableMark, Paint colour) {
Marker marker = new Marker(colour);
// marker.addArrows(MarkArrowFactory.RoundingSide.PORT, ThreadLocalRandom.current().nextDouble(91, 180), ThreadLocalRandom.current().nextDouble(1, 90));
markerObjects.put(observableMark, marker);
observableMark.addPositionListener((mark, lat, lon) -> {
Point2D p2d = findScaledXY(lat, lon);
markerObjects.get(mark).setCenterX(p2d.getX());
markerObjects.get(mark).setCenterY(p2d.getY());
markerObjects.get(mark).setLayoutX(p2d.getX());
markerObjects.get(mark).setLayoutY(p2d.getY());
});
}
@@ -295,16 +389,16 @@ public class GameView extends Pane {
private Gate makeAndBindGate(Marker m1, Marker m2, Paint colour) {
Gate gate = new Gate(colour);
gate.startXProperty().bind(
m1.centerXProperty()
m1.layoutXProperty()
);
gate.startYProperty().bind(
m1.centerYProperty()
m1.layoutYProperty()
);
gate.endXProperty().bind(
m2.centerXProperty()
m2.layoutXProperty()
);
gate.endYProperty().bind(
m2.centerYProperty()
m2.layoutYProperty()
);
return gate;
}
@@ -329,6 +423,21 @@ public class GameView extends Pane {
raceBorder.getPoints().setAll(boundaryPoints);
}
// TODO: 16/08/17 initialize zooming internal to GameView only
/**
* Enables zoom. Has to be called after this is added to a scene.
*/
public void enableZoom () {
if (this.getScene() != null) {
this.getScene().addEventHandler(KeyEvent.KEY_PRESSED, (event) -> {
if (event.getCode() == KeyCode.Z) {
zoomIn();
} else if (event.getCode() == KeyCode.X) {
zoomOut();
}
});
}
}
/**
* Rescales the race to the size of the window.
*
@@ -342,16 +451,33 @@ public class GameView extends Pane {
// drawGoogleMap();
}
private void setSelectedBoat(BoatObject bo, Boolean isSelected) {
if (this.selectedBoat == bo && !isSelected) {
this.selectedBoat = null;
boatObjects.forEach((boat, group) ->
group.setIsSelected(false)
);
} else if (isSelected) {
this.selectedBoat = bo;
for (BoatObject group : boatObjects.values()) {
if (group != bo) {
group.setIsSelected(false);
}
}
}
}
/**
* Draws all the boats.
* @param clientYachts The yachts to set in the race
* @param yachts The yachts to set in the race
*/
public void setBoats(List<ClientYacht> clientYachts) {
public void setBoats(List<ClientYacht> yachts) {
BoatObject newBoat;
final List<Group> wakes = new ArrayList<>();
for (ClientYacht clientYacht : clientYachts) {
Paint colour = clientYacht.getColour();
newBoat = new BoatObject();
newBoat.addSelectedBoatListener(this::setSelectedBoat);
newBoat.setFill(colour);
boatObjects.put(clientYacht, newBoat);
createAndBindAnnotationBox(clientYacht, colour);
@@ -364,9 +490,6 @@ public class GameView extends Pane {
BoatObject bo = boatObjects.get(boat);
Point2D p2d = findScaledXY(lat, lon);
bo.moveTo(p2d.getX(), p2d.getY(), heading, velocity, sailIn, windDir);
// annotations.get(boat).setLayoutX(p2d.getX());
// annotations.get(boat).setLayoutY(p2d.getY());
// annotations.get(boat).setLocation(100d, 100d);
annotations.get(boat).setLocation(p2d.getX(), p2d.getY());
bo.setTrajectory(
heading,
@@ -613,7 +736,6 @@ public class GameView extends Pane {
timer.stop();
}
public void setWindDir(double windDir) {
this.windDir = windDir;
}
@@ -626,10 +748,14 @@ public class GameView extends Pane {
return playerYacht;
}
public void setBoatAsPlayer (ClientYacht playerYacht) {
this.playerYacht = playerYacht;
playerYacht.toggleSail();
boatObjects.get(playerYacht).setAsPlayer();
CompoundMark currentMark = course.get(playerYacht.getLegNumber());
for (Mark mark : currentMark.getMarks()) {
markerObjects.get(mark).showNextExitArrow();
}
annotations.get(playerYacht).addAnnotation(
"velocity",
playerYacht.getVelocityProperty(),
@@ -641,6 +767,31 @@ public class GameView extends Pane {
annotationsGroup.getChildren().remove(annotations.get(playerYacht));
gameObjects.add(annotations.get(playerYacht));
});
playerYacht.addMarkRoundingListener(this::updateMarkArrows);
}
private void updateMarkArrows (ClientYacht yacht, CompoundMark compoundMark, int legNumber) {
//Only show arrows for this and next leg.
if (compoundMark != null) {
for (Mark mark : compoundMark.getMarks()) {
markerObjects.get(mark).showNextExitArrow();
}
}
CompoundMark nextMark = null;
if (legNumber < course.size() - 1) {
nextMark = course.get(legNumber);
for (Mark mark : nextMark.getMarks()) {
markerObjects.get(mark).showNextEnterArrow();
}
}
if (legNumber - 2 >= 0) {
CompoundMark lastMark = course.get(Math.max(0, legNumber - 2));
if (lastMark != nextMark) {
for (Mark mark : lastMark.getMarks()) {
markerObjects.get(mark).hideAllArrows();
}
}
}
}
/**
@@ -650,32 +801,35 @@ public class GameView extends Pane {
* @param collisionPoint yacht collision point
*/
public void drawCollision(GeoPoint collisionPoint) {
Platform.runLater(() -> {
Point2D point = findScaledXY(collisionPoint);
double circleRadius = 0.0;
Circle circle = new Circle(point.getX(), point.getY(), circleRadius, Color.RED);
gameObjects.add(circle);
Point2D point = findScaledXY(collisionPoint);
double circleRadius = 0.0;
Circle circle = new Circle(point.getX(), point.getY(), circleRadius, Color.RED);
circle.setFill(Color.TRANSPARENT);
circle.setStroke(Color.RED);
circle.setStrokeWidth(3);
circle.setFill(Color.TRANSPARENT);
circle.setStroke(Color.RED);
circle.setStrokeWidth(3);
Timeline timeline = new Timeline();
timeline.setCycleCount(1);
Timeline timeline = new Timeline();
timeline.setCycleCount(1);
KeyFrame keyframe1 = new KeyFrame(Duration.ZERO,
new KeyValue(circle.radiusProperty(), 0),
new KeyValue(circle.strokeProperty(), Color.TRANSPARENT));
KeyFrame keyFrame2 = new KeyFrame(new Duration(1000),
new KeyValue(circle.radiusProperty(), 50),
new KeyValue(circle.strokeProperty(), Color.RED));
KeyFrame keyFrame3 = new KeyFrame(new Duration(1500),
new KeyValue(circle.strokeProperty(), Color.TRANSPARENT));
KeyFrame keyframe1 = new KeyFrame(Duration.ZERO,
new KeyValue(circle.radiusProperty(), 0),
new KeyValue(circle.strokeProperty(), Color.TRANSPARENT));
KeyFrame keyFrame2 = new KeyFrame(new Duration(1000),
new KeyValue(circle.radiusProperty(), 50),
new KeyValue(circle.strokeProperty(), Color.RED));
KeyFrame keyFrame3 = new KeyFrame(new Duration(1500),
new KeyValue(circle.strokeProperty(), Color.TRANSPARENT));
timeline.getKeyFrames().addAll(keyframe1, keyFrame2, keyFrame3);
timeline.play();
timeline.getKeyFrames().addAll(keyframe1, keyFrame2, keyFrame3);
timeline.setOnFinished(event -> gameObjects.remove(circle));
});
Platform.runLater(() -> gameObjects.add(circle));
timeline.setOnFinished(event -> Platform.runLater(() -> gameObjects.remove(circle)));
timeline.play();
}
public void setFrameRateFXText(Text fpsDisplay) {
this.fpsDisplay = null;
this.fpsDisplay = fpsDisplay;
}
}
@@ -3,6 +3,7 @@ package seng302.visualiser.controllers;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.ResourceBundle;
@@ -61,9 +62,9 @@ public class FinishScreenViewController implements Initializable {
finishOrderTable.refresh();
}
public void setFinishers(List<ClientYacht> participants) {
public void setFinishers(Collection<ClientYacht> participants) {
List<ClientYacht> sorted = new ArrayList<>(participants);
sorted.sort(Comparator.comparingInt(ClientYacht::getPositionInteger));
sorted.sort(Comparator.comparingInt(ClientYacht::getPlacing));
finishOrderTable.getItems().setAll(sorted);
}
@@ -2,7 +2,6 @@ package seng302.visualiser.controllers;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Timer;
@@ -11,6 +10,8 @@ import java.util.concurrent.TimeUnit;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Point2D;
@@ -71,6 +72,10 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
private Button selectAnnotationBtn;
@FXML
private ComboBox<ClientYacht> yachtSelectionComboBox;
@FXML
private Text fpsDisplay;
@FXML
private Text windSpeedText;
//Race Data
private Map<Integer, ClientYacht> participants;
@@ -79,6 +84,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
private GameView gameView;
private RaceState raceState;
private Timeline timerTimeline;
private Timer timer = new Timer();
private List<Series<String, Double>> sparkLineData = new ArrayList<>();
private ImportantAnnotationsState importantAnnotations;
@@ -101,8 +107,8 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
public void loadRace (
Map<Integer, ClientYacht> participants, RaceXMLData raceData, RaceState raceState,
ClientYacht player
) {
ClientYacht player) {
this.participants = participants;
this.courseData = raceData;
this.markers = raceData.getCompoundMarks();
@@ -114,15 +120,38 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
initialiseBoatSelectionComboBox();
initialiseSparkLine();
raceState.getPlayerPositions().addListener((ListChangeListener<ClientYacht>) c -> {
while (c.next()) {
if (c.wasPermutated()) {
updateOrder(raceState.getPlayerPositions());
}
}
});
updateOrder(raceState.getPlayerPositions());
gameView = new GameView();
Platform.runLater(() -> contentAnchorPane.getChildren().add(gameView));
gameView.setBoats(new ArrayList<>(participants.values()));
gameView.updateBorder(raceData.getCourseLimit());
gameView.updateCourse(
new ArrayList<>(raceData.getCompoundMarks().values()), raceData.getMarkSequence()
gameView.setFrameRateFXText(fpsDisplay);
Platform.runLater(() -> contentAnchorPane.getChildren().add(0, gameView));
gameView.setBoats(new ArrayList<>(participants.values()));
gameView.updateBorder(raceData.getCourseLimit());
gameView.updateCourse(
new ArrayList<>(raceData.getCompoundMarks().values()), raceData.getMarkSequence()
);
gameView.enableZoom();
gameView.setBoatAsPlayer(player);
gameView.startRace();
raceState.addCollisionListener(gameView::drawCollision);
raceState.windDirectionProperty().addListener((obs, oldDirection, newDirection) -> {
gameView.setWindDir(newDirection.doubleValue());
updateWindDirection(newDirection.doubleValue());
});
raceState.windSpeedProperty().addListener((obs, oldSpeed, newSpeed) -> {
updateWindSpeed(newSpeed.doubleValue());
});
updateWindDirection(raceState.windDirectionProperty().doubleValue());
updateWindSpeed(raceState.getWindSpeed());
gameView.setWindDir(raceState.windDirectionProperty().doubleValue());
}
/**
@@ -208,10 +237,9 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
/**
* Used to add any new yachts into the race that may have started late or not have had data
* received yet
* Used to add any new yachts into the race that may have started late or not have had data received yet
*/
private void updateSparkLine() {
private void updateSparkLine(){
// TODO: 2/08/17 there is about 0 chance of this working. Once we are keeping track of boat positions it can be fixed.
// Collect the racing yachts that aren't already in the chart
sparkLineData.clear();
@@ -219,40 +247,39 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
// Create a new data series for new yachts
sparkLineCandidates
.stream()
.filter(yacht -> yacht.getPositionInteger() != null)
.filter(yacht -> yacht.getPlacing() != null)
.forEach(yacht -> {
Series<String, Double> yachtData = new Series<>();
yachtData.setName(yacht.getSourceId().toString());
yachtData.getData().add(
new XYChart.Data<>(
Integer.toString(yacht.getLegNumber()),
1.0 + participants.size() - yacht.getPositionInteger()
1.0 + participants.size() - yacht.getPlacing()
)
);
sparkLineData.add(yachtData);
sparkLineData.add(yachtData);
});
// Lambda function to sort the series in order of leg (later legs shown more to the right)
sparkLineData.sort((o1, o2) -> {
Integer leg1 = Integer.parseInt(o1.getData().get(o1.getData().size() - 1).getXValue());
Integer leg2 = Integer.parseInt(o2.getData().get(o2.getData().size() - 1).getXValue());
if (leg2 < leg1) {
Integer leg1 = Integer.parseInt(o1.getData().get(o1.getData().size()-1).getXValue());
Integer leg2 = Integer.parseInt(o2.getData().get(o2.getData().size()-1).getXValue());
if (leg2 < leg1){
return 1;
} else {
return -1;
}
});
// Adds the new data series to the sparkline (and set the colour of the series)
Platform.runLater(() ->
Platform.runLater(() -> {
sparkLineData
.stream()
.filter(spark -> !raceSparkLine.getData().contains(spark))
.forEach(spark -> {
raceSparkLine.getData().add(spark);
spark.getNode().lookup(".chart-series-line")
.setStyle("-fx-stroke:" + getBoatColorAsRGB(spark.getName()));
})
);
spark.getNode().lookup(".chart-series-line").setStyle("-fx-stroke:" + getBoatColorAsRGB(spark.getName()));
});
});
}
private void initialiseSparkLine() {
@@ -262,15 +289,15 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
/**
* Updates the yachts sparkline of the desired yacht and using the new leg number
* @param clientYacht The yacht to be updated on the sparkline
* @param yacht The yacht to be updated on the sparkline
* @param legNumber the leg number that the position will be assigned to
*/
void updateYachtPositionSparkline(ClientYacht clientYacht, Integer legNumber) {
void updateYachtPositionSparkline(ClientYacht yacht, Integer legNumber){
for (XYChart.Series<String, Double> positionData : sparkLineData) {
positionData.getData().add(
new Data<>(
Integer.toString(legNumber),
1.0 + participants.size() - clientYacht.getPositionInteger()
1.0 + participants.size() - yacht.getPlacing()
)
);
}
@@ -278,7 +305,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
// positionData.getData().add(
// new XYChart.Data<>(
// Integer.toString(legNumber),
// 1.0 + participants.size() - yacht.getPosition()
// 1.0 + participants.size() - yacht.getPlacing()
// )
// );
}
@@ -286,52 +313,47 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
/**
* gets the rgb string of the yachts colour to use for the chart via css
*
* @param yachtId id of yacht passed in to get the yachts colour
* @return the colour as an rgb string
*/
private String getBoatColorAsRGB(String yachtId) {
private String getBoatColorAsRGB(String yachtId){
Color color = participants.get(Integer.valueOf(yachtId)).getColour();
if (color == null) {
return String.format("#%02X%02X%02X", 255, 255, 255);
if (color == null){
return String.format("#%02X%02X%02X",255,255,255);
}
return String.format("#%02X%02X%02X",
(int) (color.getRed() * 255),
(int) (color.getGreen() * 255),
(int) (color.getBlue() * 255)
return String.format( "#%02X%02X%02X",
(int)( color.getRed() * 255 ),
(int)( color.getGreen() * 255 ),
(int)( color.getBlue() * 255 )
);
}
/**
* Initialises a timer which updates elements of the RaceView such as wind direction, yacht
* orderings etc.. which are dependent on the info from the stream parser constantly. Updates of
* each of these attributes are called ONCE EACH SECOND
* orderings etc.. which are dependent on the info from the stream parser constantly.
* Updates of each of these attributes are called ONCE EACH SECOND
*/
private void initializeUpdateTimer() {
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
updateRaceTime();
updateWindDirection();
updateOrder();
// updateSparkLine();
}
}, 0, 1000);
}
/**
* Iterates over all corners until ones SeqID matches with the yachts current leg number. Then
* it gets the compoundMarkID of that corner and uses it to fetch the appropriate mark Returns
* null if no next mark found.
*
* Iterates over all corners until ones SeqID matches with the yachts current leg number.
* Then it gets the compoundMarkID of that corner and uses it to fetch the appropriate mark
* Returns null if no next mark found.
* @param bg The BoatGroup to find the next mark of
* @return The next Mark or null if none found
*/
private Mark getNextMark(BoatObject bg) {
// TODO: 1/08/17 Move to GameView
//
// Integer legNumber = bg.getYacht().getLegNumber();
// Integer legNumber = bg.getClientYacht().getLegNumber();
// List<Corner> markSequence = courseData.getMarkSequence();
//
// if (legNumber == 0) {
@@ -352,10 +374,19 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
/**
* Updates the wind direction arrow and text as from info from the StreamParser
* @param direction the from north angle of the wind.
*/
private void updateWindDirection() {
windDirectionText.setText(String.format("%.1f°", raceState.getWindDirection()));
windArrowText.setRotate(raceState.getWindDirection());
private void updateWindDirection(double direction) {
windDirectionText.setText(String.format("%.1f°", direction));
windArrowText.setRotate(direction);
}
/**
* Updates the speed of the wind as displayed by the info pane.
* @param windSpeed Windspeed in knots.
*/
private void updateWindSpeed(double windSpeed) {
windSpeedText.setText("Speed: " + String.format("%.1f", windSpeed) + " Knots");
}
@@ -375,26 +406,20 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
* Updates the order of the yachts as from the StreamParser and sets them in the yacht order
* section
*/
private void updateOrder() {
// positionVbox.getChildren().removeAll();
// positionVbox.getStylesheets().add(getClass().getResource("/css/master.css").toString());
// list of racing yacht id
List<ClientYacht> sorted = new ArrayList<>(participants.values());
sorted.sort(Comparator.comparingInt(ClientYacht::getPositionInteger));
private void updateOrder(ObservableList<ClientYacht> yachts) {
List<Text> vboxEntries = new ArrayList<>();
for (ClientYacht clientYacht : sorted) {
for (int i = 0; i < yachts.size(); i++) {
// System.out.println("yacht == null " + String.valueOf(yacht == null));
if (clientYacht.getBoatStatus() == 3) { // 3 is finish status
Text textToAdd = new Text(clientYacht.getPositionInteger() + ". " +
clientYacht.getShortName() + " (Finished)");
if (yachts.get(i).getBoatStatus() == 3) { // 3 is finish status
Text textToAdd = new Text(i + 1 + ". " +
yachts.get(i).getShortName() + " (Finished)");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
vboxEntries.add(textToAdd);
} else {
Text textToAdd = new Text(clientYacht.getPositionInteger() + ". " +
clientYacht.getShortName() + " ");
Text textToAdd = new Text(i + 1 + ". " +
yachts.get(i).getShortName() + " ");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
textToAdd.setStyle("");
vboxEntries.add(textToAdd);
@@ -403,13 +428,6 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
Platform.runLater(() ->
positionVbox.getChildren().setAll(vboxEntries)
);
// participants.forEach((id, yacht) ->{
// Text textToAdd = new Text(yacht.getPosition() + ". " +
// yacht.getShortName() + " ");
// textToAdd.setFill(Paint.valueOf("#d3d3d3"));
// textToAdd.setStyle("");
// positionVbox.getChildren().add(textToAdd);
// });
}
@@ -488,8 +506,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
}
public Line makeLeftLayline(Point2D startPoint, Double layLineAngle, Double baseAngle) {
public Line makeLeftLayline(Point2D startPoint, Double layLineAngle, Double baseAngle) {
Point2D ep = getPointRotation(startPoint, 50.0, baseAngle + layLineAngle);
Line line = new Line(startPoint.getX(), startPoint.getY(), ep.getX(), ep.getY());
return line;
@@ -507,8 +524,8 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
/**
* Initialised the combo box with any yachts currently in the race and adds the required
* listener for the combobox to take action upon selection
* Initialised the combo box with any yachts currently in the race and adds the required listener
* for the combobox to take action upon selection
*/
private void initialiseBoatSelectionComboBox() {
yachtSelectionComboBox.setItems(
@@ -580,9 +597,9 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
/**
* Sets all the annotations of the selected yacht to be visible and all others to be hidden
*
* @param clientYacht The yacht for which we want to view all annotations
* @param yacht The yacht for which we want to view all annotations
*/
private void setSelectedBoat(ClientYacht clientYacht) {
private void setSelectedBoat(ClientYacht yacht) {
// for (BoatObject bg : gameViewController.getBoatGroups()) {
// //We need to iterate over all race groups to get the matching yacht group belonging to this yacht if we
// //are to toggle its annotations, there is no other backwards knowledge of a yacht to its yachtgroup.
@@ -596,23 +613,8 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
// }
}
public void updateRaceData(RaceXMLData raceData) {
public void updateRaceData (RaceXMLData raceData) {
this.courseData = raceData;
gameView.updateBorder(raceData.getCourseLimit());
}
/**
* Called by game client after receiving yacht event packet. Parameter subject id is the
* offending yacht. This function in turn will pass the yacht location to game view to display a
* collision alert.
*
* @param subjectId source id of offending yacht
*/
public void showCollision(Long subjectId) {
gameView.drawCollision(participants.get((int) (long) subjectId).getLocation());
}
public GameView getGameView() {
return gameView;
}
}
@@ -95,7 +95,7 @@ public class AnnotationBox extends Group {
background.setStroke(theme);
background.setStrokeWidth(2);
background.setCache(true);
background.setCacheHint(CacheHint.SPEED);
background.setCacheHint(CacheHint.SCALE);
this.getChildren().add(background);
}
@@ -213,7 +213,7 @@ public class AnnotationBox extends Group {
Text text = new Text();
text.setFill(theme);
text.setStrokeWidth(2);
text.setCacheHint(CacheHint.SPEED);
// text.setCacheHint(CacheHint.QUALITY);
text.setCache(true);
return text;
}
@@ -1,9 +1,9 @@
package seng302.visualiser.fxObjects;
import java.util.ArrayList;
import java.util.List;
import javafx.application.Platform;
import javafx.geometry.Point2D;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
@@ -11,6 +11,7 @@ import javafx.scene.paint.Paint;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Polyline;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.transform.Rotate;
/**
@@ -23,6 +24,12 @@ import javafx.scene.transform.Rotate;
*/
public class BoatObject extends Group {
@FunctionalInterface
public interface SelectedBoatListener {
void notifySelected(BoatObject boatObject, Boolean isSelected);
}
//Constants for drawing
private static final double BOAT_HEIGHT = 15d;
private static final double BOAT_WIDTH = 10d;
@@ -41,9 +48,11 @@ public class BoatObject extends Group {
private double distanceTravelled, lastRotation;
private Point2D lastPoint;
private Paint colour = Color.BLACK;
private Boolean isSelected, destinationSet; //All boats are initialised as selected
private Boolean isSelected = false, destinationSet; //All boats are initialised as selected
private boolean isPlayer = false;
private List<SelectedBoatListener> selectedBoatListenerListeners = new ArrayList<>();
/**
* Creates a BoatGroup with the default triangular boat polygon.
*/
@@ -85,7 +94,7 @@ public class BoatObject extends Group {
});
boatPoly.setOnMouseClicked(event -> setIsSelected(!isSelected));
boatPoly.setCache(true);
boatPoly.setCacheHint(CacheHint.SPEED);
// boatPoly.setCacheHint(CacheHint.SPEED);
// annotationBox = new AnnotationBox();
// annotationBox.setFill(colour);
@@ -287,6 +296,7 @@ public class BoatObject extends Group {
// }
public void setIsSelected(Boolean isSelected) {
updateListener(isSelected);
this.isSelected = isSelected;
setLineGroupVisible(isSelected);
setWakeVisible(isSelected);
@@ -352,7 +362,8 @@ public class BoatObject extends Group {
BOAT_WIDTH / 1.75, BOAT_HEIGHT / 1.75
);
boatPoly.setStroke(Color.BLACK);
boatPoly.setStrokeWidth(3);
boatPoly.setStrokeWidth(2);
boatPoly.setStrokeLineCap(StrokeLineCap.ROUND);
isPlayer = true;
animateSail();
}
@@ -365,6 +376,10 @@ public class BoatObject extends Group {
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);
@@ -372,4 +387,14 @@ public class BoatObject extends Group {
// 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,147 @@
package seng302.visualiser.fxObjects;
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;
}
}
@@ -1,19 +1,98 @@
package seng302.visualiser.fxObjects;
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;
/**
* Visual object for a mark.
* Visual object for a mark. Contains a coloured circle and any specified arrows.
*/
public class Marker extends Circle {
public class Marker extends Group {
private Circle mark = new Circle();
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() {
super.setRadius(5);
mark.setRadius(5);
mark.setCenterX(0);
mark.setCenterY(0);
Platform.runLater(() -> 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();
super.setFill(colour);
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) {
enterArrows.add(
MarkArrowFactory.constructEntryArrow(roundingSide, entryAngle, exitAngle, colour)
);
exitArrows.add(
MarkArrowFactory.constructExitArrow(roundingSide, exitAngle, colour)
);
// Platform.runLater(() -> {
// this.getChildren().add(enterArrows.get(enterArrows.size()-1));
// this.getChildren().add(exitArrows.get(exitArrows.size()-1));
// });
}
/**
* 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()));
}
}