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

This commit is contained in:
Michael Rausch
2017-07-27 11:06:00 +12:00
23 changed files with 423 additions and 249 deletions
@@ -59,6 +59,7 @@ public class ClientPacketParser {
*/
public ClientPacketParser() {
}
/**
* Looks at the type of the packet then sends it to the appropriate parser to extract the
* specific data associated with that packet type
@@ -78,7 +79,6 @@ public class ClientPacketParser {
extractDisplayMessage(packet);
break;
case XML_MESSAGE:
newRaceXmlReceived = true;
extractXmlMessage(packet);
break;
case RACE_START_STATUS:
@@ -108,7 +108,7 @@ public class ClientPacketParser {
}
} catch (NullPointerException e) {
System.out.println("Error parsing packet");
e.printStackTrace();
// e.printStackTrace();
}
}
@@ -185,7 +185,6 @@ public class ClientPacketParser {
int noBoats = payload[22];
int raceType = payload[23];
clientStateBoats = ClientState.getBoats();
for (int i = 0; i < noBoats; i++) {
long boatStatusSourceID = bytesToLong(
Arrays.copyOfRange(payload, 24 + (i * 20), 28 + (i * 20)));
@@ -206,7 +205,9 @@ public class ClientPacketParser {
boat.setEstimateTimeAtNextMark(estTimeAtNextMark);
boat.setEstimateTimeAtFinish(estTimeAtFinish);
Yacht clientBoat = clientStateBoats.get((int) boatStatusSourceID);
// Update Client State boats when receive race status packet.
// Potentially could replace boats in ClientPacketParser.
Yacht clientBoat = ClientState.getBoats().get((int) boatStatusSourceID);
clientBoat.setBoatStatus((boatStatus));
setBoatLegPosition(clientBoat, boatLegNumber);
clientBoat.setPenaltiesAwarded(boatPenaltyAwarded);
@@ -215,26 +216,32 @@ public class ClientPacketParser {
clientBoat.setEstimateTimeAtFinish(estTimeAtFinish);
}
// 3 is race started
// 3 is race started.
// ClientState race started flag will be set to true if race started, else set false.
if (raceStatus == 3) {
ClientState.setRaceStarted(true);
} else {
ClientState.setRaceStarted(false);
}
}
private static void setBoatLegPosition(Yacht updatingBoat, Integer leg){
Integer placing = 1;
if (leg != updatingBoat.getLegNumber() && (raceStarted || raceFinished)) {
if (/* TODO implement when we are getting this data /TODO leg != updatingBoat.getLegNumber() && */(raceStarted || raceFinished)) {
for (Yacht boat : boats.values()) {
placing = boat.getSourceId();
/* See above to-do
if (boat.getLegNumber() != null && leg <= boat.getLegNumber()){
placing += 1;
}
}*/
}
updatingBoat.setPosition(placing.toString());
updatingBoat.setLegNumber(leg);
boatsPos.putIfAbsent(placing, updatingBoat);
boatsPos.replace(placing, updatingBoat);
} else if(updatingBoat.getLegNumber() == null){
updatingBoat.setPosition("1");
updatingBoat.setPosition("-");
updatingBoat.setLegNumber(leg);
}
}
@@ -284,10 +291,13 @@ public class ClientPacketParser {
}
xmlObject.constructXML(doc, messageType);
if (messageType == 7) { //7 is the boat XML
boats = xmlObject.getBoatXML().getCompetingBoats();
// Set/Update the ClientState boats after receiving new boat xml.
// Flag boatsUpdated in ClientState to true.
ClientState.setBoats(xmlObject.getBoatXML().getCompetingBoats());
ClientState.setDirtyState(true);
ClientState.setBoatsUpdated(true);
}
if (messageType == 6) { //6 is race info xml
newRaceXmlReceived = true;
@@ -1,14 +1,12 @@
package seng302.client;
import com.sun.org.apache.xpath.internal.operations.Bool;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import seng302.models.Yacht;
/**
* Used by the client to store static variables to be used in game.
* Used by the client to store static variables, which other threads and classes
* observer so that they can update their status accordingly.
*/
public class ClientState {
@@ -17,7 +15,7 @@ public class ClientState {
private static Boolean raceStarted = false;
private static Boolean connectedToHost = false;
private static Map<Integer, Yacht> boats = new ConcurrentHashMap<>();
private static Boolean dirtyState = true;
private static Boolean boatsUpdated = true;
private static String clientSourceId = "";
public static String getHostIp() {
@@ -56,12 +54,12 @@ public class ClientState {
return boats;
}
public static Boolean isDirtyState() {
return dirtyState;
public static Boolean isBoatsUpdated() {
return boatsUpdated;
}
public static void setDirtyState(Boolean dirtyState) {
ClientState.dirtyState = dirtyState;
public static void setBoatsUpdated(Boolean boatsUpdated) {
ClientState.boatsUpdated = boatsUpdated;
}
public static String getClientSourceId() {
@@ -12,6 +12,12 @@ public class ClientStateQueryingRunnable extends Observable implements Runnable
public ClientStateQueryingRunnable() {}
/**
* Notifies observers(the lobby controller) that "game started" if ClientState
* raceStarted flag is true and terminates itself. Also, it notifies observers
* to add/remove players if ClientState boatsUpdated flag is true, then resets
* the flag to false;
*/
@Override
public void run() {
while(!terminate) {
@@ -29,14 +35,19 @@ public class ClientStateQueryingRunnable extends Observable implements Runnable
terminate();
}
if (ClientState.isDirtyState()) {
if (ClientState.isBoatsUpdated()) {
setChanged();
notifyObservers("update players");
ClientState.setDirtyState(false);
ClientState.setBoatsUpdated(false);
}
}
}
/**
* Used to terminate the thread.
*
* Currently called by the main while loop when game started is detected.
*/
public void terminate() {
terminate = true;
}
@@ -15,7 +15,8 @@ import seng302.server.messages.BoatActionMessage;
import seng302.server.messages.Message;
/**
* Created by kre39 on 13/07/17.
* A class describing a single connection to a Server for the purposes of sending and receiving on
* its own thread.
*/
public class ClientToServerThread implements Runnable {
@@ -32,6 +33,17 @@ public class ClientToServerThread implements Runnable {
private Boolean updateClient = true;
private ByteArrayOutputStream crcBuffer;
/**
* Constructor for ClientToServerThread which takes in ipAddress and portNumber and attempts to
* connect to the specified ipAddress and port.
*
* Upon successful socket connection, threeWayHandshake will be preformed and the instance will
* be put on a thread and run immediately.
*
* @param ipAddress a string of ip address to be connected to
* @param portNumber an integer port number
* @throws Exception SocketConnection if fail to connect to ip address and port number combination
*/
public ClientToServerThread(String ipAddress, Integer portNumber) throws Exception{
socket = new Socket(ipAddress, portNumber);
is = socket.getInputStream();
@@ -40,7 +52,7 @@ public class ClientToServerThread implements Runnable {
Integer allocatedID = threeWayHandshake();
if (allocatedID != null) {
ourID = allocatedID;
clientLog("Successful handshake. Allocated ID: " + ourID, 1);
clientLog("Successful handshake. Allocated ID: " + ourID, 0);
ClientState.setClientSourceId(String.valueOf(ourID));
} else {
clientLog("Unsuccessful handshake", 1);
@@ -50,31 +62,31 @@ public class ClientToServerThread implements Runnable {
thread = new Thread(this);
thread.start();
}
/**
* Prints out log messages and the time happened.
* Only perform task if log level is below LOG_LEVEL variable.
*
* @param message a string of message to be printed out
* @param logLevel an int for log level
*/
static void clientLog(String message, int logLevel){
if(logLevel <= LOG_LEVEL){
System.out.println("[CLIENT " + LocalDateTime.now().toLocalTime().toString() + "] " + message);
}
}
/**
* Perform the thread loop. It exits the loop if ClientState connected to host
* variable is false.
*/
public void run() {
int sync1;
int sync2;
// TODO: 14/07/17 wmu16 - Work out how to fix this while loop
while(ClientState.isConnectedToHost()) {
try {
//Perform a write if it is time to as delegated by the MainServerThread
if (updateClient) {
// TODO: 13/07/17 wmu16 - Write out game state - some function that would write all appropriate messages to this output stream
// try {
// GameState.outputState(os);
// } catch (IOException e) {
// System.out.println("IO error in server thread upon writing to output stream");
// }
updateClient = false;
}
crcBuffer = new ByteArrayOutputStream();
sync1 = readByte();
sync2 = readByte();
@@ -93,15 +105,13 @@ public class ClientToServerThread implements Runnable {
if (computedCrc == packetCrc) {
ClientPacketParser
.parsePacket(new StreamPacket(type, payloadLength, timeStamp, payload));
// TODO: 17/07/17 wmu16 - Fix this or maybe we dont need to go through the main server at all!?!?
// packetBufferDelegate.addToBuffer(new StreamPacket(type, payloadLength, timeStamp, payload));
} else {
clientLog("Packet has been dropped", 1);
}
}
} catch (Exception e) {
closeSocket();
e.printStackTrace();
clientLog("Disconnected from server", 1);
return;
}
}
@@ -111,7 +121,7 @@ public class ClientToServerThread implements Runnable {
/**
* Listens for an allocated sourceID and returns it to the server if recieved
* Listens for an allocated sourceID and returns it to the server
* @return the sourceID allocated to us by the server
*/
private Integer threeWayHandshake() {
@@ -120,14 +130,15 @@ public class ClientToServerThread implements Runnable {
try {
ourSourceID = is.read();
} catch (IOException e) {
e.printStackTrace();
clientLog("Three way handshake failed", 1);
}
if (ourSourceID != null) {
try {
os.write(ourSourceID);
return ourSourceID;
} catch (IOException e) {
e.printStackTrace();
clientLog("Three way handshake failed", 1);
return null;
}
}
@@ -143,8 +154,7 @@ public class ClientToServerThread implements Runnable {
try {
os.write(boatActionMessage.getBuffer());
} catch (IOException e) {
clientLog("COULD NOT WRITE TO SERVER", 0);
e.printStackTrace();
clientLog("Could not write to server", 1);
}
}
@@ -153,7 +163,7 @@ public class ClientToServerThread implements Runnable {
try {
socket.close();
} catch (IOException e) {
clientLog("Failed to close the socket", 0);
clientLog("Failed to close the socket", 1);
}
}
@@ -164,7 +174,7 @@ public class ClientToServerThread implements Runnable {
currentByte = is.read();
crcBuffer.write(currentByte);
} catch (IOException e) {
e.printStackTrace();
clientLog("Read byte failed", 1);
}
if (currentByte == -1){
throw new Exception();
@@ -22,6 +22,7 @@ import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.scene.text.Text;
import seng302.client.ClientPacketParser;
import seng302.client.ClientState;
import seng302.fxObjects.BoatGroup;
import seng302.models.Colors;
import seng302.models.Yacht;
@@ -57,8 +58,8 @@ public class CanvasController {
private final int BUFFER_SIZE = 50;
private final int PANEL_WIDTH = 1260; // it should be 1280 but, minors 40 to cancel the bias.
private final int PANEL_HEIGHT = 960;
private final int CANVAS_WIDTH = 720;
private final int CANVAS_HEIGHT = 720;
private final int CANVAS_WIDTH = 1100;
private final int CANVAS_HEIGHT = 920;
private boolean horizontalInversion = false;
private double distanceScaleFactor;
@@ -74,7 +75,7 @@ public class CanvasController {
private List<MarkGroup> markGroups = new ArrayList<>();
private List<BoatGroup> boatGroups = new ArrayList<>();
private Text FPSdisplay = new Text();
private Text FPSDisplay = new Text();
private Polygon raceBorder = new Polygon();
//FRAME RATE
@@ -119,10 +120,10 @@ public class CanvasController {
gc.setGlobalAlpha(0.5);
fitMarksToCanvas();
drawGoogleMap();
FPSdisplay.setLayoutX(5);
FPSdisplay.setLayoutY(20);
FPSdisplay.setStrokeWidth(2);
group.getChildren().add(FPSdisplay);
FPSDisplay.setLayoutX(5);
FPSDisplay.setLayoutY(20);
FPSDisplay.setStrokeWidth(2);
group.getChildren().add(FPSDisplay);
group.getChildren().add(raceBorder);
initializeMarks();
initializeBoats();
@@ -249,12 +250,9 @@ public class CanvasController {
// some raceObjects will have multiple ID's (for instance gate marks)
//checking if the current "ID" has any updates associated with it
if (ClientPacketParser.boatLocations.containsKey(boatGroup.getRaceId())) {
if (boatGroup.isStopped()) {
updateBoatGroup(boatGroup);
}
}
boatGroup.move();
}
for (MarkGroup markGroup : markGroups) {
for (Long id : markGroup.getRaceIds()) {
if (ClientPacketParser.markLocations.containsKey(id)) {
@@ -324,10 +322,25 @@ public class CanvasController {
if (participantIDs.contains(boat.getSourceId())) {
boat.setColour(Colors.getColor());
BoatGroup boatGroup = new BoatGroup(boat, boat.getColour());
if (boat.getSourceId().equals(Integer.parseInt(ClientState.getClientSourceId()))) {
boatGroup.setAsPlayer();
boatGroups.add(boatGroup);
annotations.getChildren().add(boatGroup.getAnnotations());
} else {
//Move annotations and boat to bottom of group keeping player ontop.
if (boatGroups.size() > 0) {
boatGroups.add(0, boatGroup);
} else {
boatGroups.add(boatGroup);
}
if (annotations.getChildren().size() > 0) {
annotations.getChildren().add(0, boatGroup.getAnnotations());
} else {
annotations.getChildren().add(boatGroup.getAnnotations());
}
}
trails.getChildren().add(boatGroup.getTrail());
wakes.getChildren().add(boatGroup.getWake());
annotations.getChildren().add(boatGroup.getAnnotations());
}
}
group.getChildren().addAll(trails);
@@ -391,10 +404,10 @@ public class CanvasController {
private void drawFps(int fps){
if (raceViewController.isDisplayFps()){
FPSdisplay.setVisible(true);
FPSdisplay.setText(String.format("%d FPS", fps));
FPSDisplay.setVisible(true);
FPSDisplay.setText(String.format("%d FPS", fps));
} else {
FPSdisplay.setVisible(false);
FPSDisplay.setVisible(false);
}
}
@@ -112,6 +112,7 @@ public class LobbyController implements Initializable, Observer{
readyButton.setDisable(true);
}
// put all javafx objects in lists, so we can iterate though conveniently
imageViews = new ArrayList<>();
Collections.addAll(imageViews, firstImageView, secondImageView, thirdImageView, fourthImageView,
fifthImageView, sixthImageView, seventhImageView, eighthImageView);
@@ -134,6 +135,13 @@ public class LobbyController implements Initializable, Observer{
clientStateQueryingThread.start();
}
/**
* Observers "ClientStateQueryingRunnable".
* When the clients state has been marked to "race start", the querying thread
* will notify this lobby to change the view
* @param o
* @param arg
*/
@Override
public void update(Observable o, Object arg) {
Platform.runLater(new Runnable() {
@@ -149,6 +157,9 @@ public class LobbyController implements Initializable, Observer{
});
}
/**
* Reset all ListViews and ImageViews according to the current competitors
*/
private void initialiseListView() {
listViews.forEach(listView -> listView.getItems().clear());
imageViews.forEach(gif -> gif.setVisible(false));
@@ -156,12 +167,15 @@ public class LobbyController implements Initializable, Observer{
List<Integer> ids = new ArrayList<>(ClientState.getBoats().keySet());
for (int i = 0; i < ids.size(); i++) {
competitors.get(i).add(String.format("Player ID: %d", ids.get(i)));
competitors.get(i).add(ClientState.getBoats().get(ids.get(i)).getBoatName());
listViews.get(i).setItems(competitors.get(i));
imageViews.get(i).setVisible(true);
}
}
/**
* Loads preset images into imageViews
*/
private void initialiseImageView() {
for (int i = 0; i < MAX_NUM_PLAYERS; i++) {
imageViews.get(i).setImage(new Image(getClass().getResourceAsStream("/pics/sail.png")));
@@ -195,31 +209,11 @@ public class LobbyController implements Initializable, Observer{
@FXML
public void readyButtonPressed() {
// setContentPane("/views/RaceView.fxml");
// playTheme();
GameState.setCurrentStage(GameStages.RACING);
mainServerThread.startGame();
}
// private static MediaPlayer mediaPlayer;
//
// private void playTheme() {
// Random random = new Random(System.currentTimeMillis());
// Integer rand = random.nextInt();
// if(rand == 10) {
// URL file = getClass().getResource("/music/Disturbed - down with the sickness.mp3");
// Media hit = new Media(file.toString());
// mediaPlayer = new MediaPlayer(hit);
// mediaPlayer.play();
// } else if(rand == 9) {
// URL file = getClass().getResource("/music/Owl City - Fireflies.mp3");
// Media hit = new Media(file.toString());
// mediaPlayer = new MediaPlayer(hit);
// mediaPlayer.play();
// }
// }
private void switchToRaceView() {
if (!switchedPane) {
switchedPane = true;
@@ -287,6 +287,7 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
updateWindDirection();
// updateOrder();
updateBoatSelectionComboBox();
updateOrder();
})
);
@@ -383,9 +384,12 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
}
if (ClientPacketParser.isRaceStarted()) {
/*
for (Yacht boat : ClientPacketParser.getBoatsPos().values()) {
if (participantIDs.contains(boat.getSourceId())) { // check if the boat is racing
if (boat.getBoatStatus() == 3) { // 3 is finish status
System.out.println("Hi tjere" + boat.getBoatName());
if (participantIDs.contains(boat.getSourceId()) || true
) { // check if the boat is racing
if (boat.getBoatStatus() == 69) { // 3 is finish status
Text textToAdd = new Text(boat.getPosition() + ". " +
boat.getShortName() + " (Finished)");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
@@ -397,9 +401,17 @@ public class RaceViewController extends Thread implements ImportantAnnotationDel
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
textToAdd.setStyle("");
positionVbox.getChildren().add(textToAdd);
System.out.println("Adding " + textToAdd.getText());
}
}
}
*/
for (Yacht boat : ClientPacketParser.getBoats().values()){
Text textToAdd = new Text(boat.getSourceId() + ". " + boat.getShortName() + " ");
textToAdd.setFill(Paint.valueOf("#d3d3d3"));
textToAdd.setStyle("");
positionVbox.getChildren().add(textToAdd);
}
} else {
for (Yacht boat : ClientPacketParser.getBoats().values()) {
if (participantIDs.contains(boat.getSourceId())) { // check if the boat is racing
@@ -135,7 +135,11 @@ public class ImportantAnnotationController implements Initializable {
boatEstTimeToNextMarkSelect.isSelected()));
boatElapsedTimeSelect.setOnAction(
event -> setAnnotation(Annotation.LEGTIME, boatElapsedTimeSelect.isSelected()));
// TODO: 26/07/17 cir27 - Create a more robust fix for this when the annotation for the game are decided upon.
boatEstTimeToNextMarkSelect.setVisible(false);
boatEstTimeToNextMarkSelect.setDisable(true);
boatElapsedTimeSelect.setVisible(false);
boatElapsedTimeSelect.setDisable(true);
closeButton.setOnAction(event -> stage.close());
}
}
@@ -33,7 +33,7 @@ public class BoatAnnotations extends Group{
private Text velocityObject;
private Text estTimeToNextMarkObject;
private Text legTimeObject;
private boolean isPlayer = false;
private Yacht boat;
BoatAnnotations (Yacht boat, Color theme) {
@@ -56,12 +56,17 @@ public class BoatAnnotations extends Group{
velocityObject = getTextObject("0 m/s", theme);
velocityObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 2);
velocityObject.setVisible(false);
estTimeToNextMarkObject = getTextObject("Next mark: ", theme);
estTimeToNextMarkObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 3);
estTimeToNextMarkObject.setVisible(false);
legTimeObject = getTextObject("Last mark: -", theme);
legTimeObject.relocate(X_OFFSET_TEXT, Y_OFFSET_TEXT_INIT + Y_OFFSET_PER_TEXT * 4);
legTimeObject.setVisible(false);
this.setVisible(true, false, false, false);
super.getChildren().addAll(background, teamNameObject, velocityObject, estTimeToNextMarkObject, legTimeObject);
}
@@ -83,6 +88,8 @@ public class BoatAnnotations extends Group{
}
void update () {
teamNameObject.setText("Player: " + boat.getShortName());
velocityObject.setText(String.format(String.format("%.2f m/s", boat.getVelocityMMS())));
if (boat.getTimeTillNext() != null) {
@@ -104,13 +111,19 @@ public class BoatAnnotations extends Group{
}
}
void setVisibile (boolean nameVisibility, boolean speedVisibility,
void setVisible(boolean nameVisibility, boolean speedVisibility,
boolean estTimeVisibility, boolean lastMarkVisibility) {
int totalVisible = 0;
/*
This is a temporary fix until the new annotation group is added along with the visualiser
overhaul.
*/
totalVisible = updateVisibility(nameVisibility, teamNameObject, totalVisible);
if (isPlayer)
totalVisible = updateVisibility(speedVisibility, velocityObject, totalVisible);
totalVisible = updateVisibility(estTimeVisibility, estTimeToNextMarkObject, totalVisible);
totalVisible = updateVisibility(lastMarkVisibility, legTimeObject, totalVisible);
// totalVisible = updateVisibility(estTimeVisibility, estTimeToNextMarkObject, totalVisible);
// totalVisible = updateVisibility(lastMarkVisibility, legTimeObject, totalVisible);
if (totalVisible != 0) {
background.setVisible(true);
background.setHeight(Math.abs(BACKGROUND_X) + TEXT_BUFFER + BACKGROUND_H_PER_TEXT * totalVisible);
@@ -130,4 +143,12 @@ public class BoatAnnotations extends Group{
}
return totalVisible;
}
/**
* Sets these annotations to show more detailed info.
*/
public void setAsPlayer () {
isPlayer = true;
velocityObject.setVisible(true);
}
}
+50 -53
View File
@@ -6,6 +6,7 @@ import javafx.geometry.Point2D;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.transform.Rotate;
@@ -48,8 +49,9 @@ public class BoatGroup extends Group {
private Point2D lastPoint;
private boolean destinationSet;
private BoatAnnotations boatAnnotations;
private Boolean isSelected = true; //All boats are initialised as selected
private Color color;
private Boolean isSelected = true; //All boats are initialised as selected\
private boolean isPlayer = false;
/**
* Creates a BoatGroup with the default triangular boat polygon.
@@ -88,20 +90,21 @@ public class BoatGroup extends Group {
* polygon.
*/
private void initChildren(Color color, double... points) {
this.color = color;
boatPoly = new Polygon(points);
boatPoly.setFill(color);
boatPoly.setFill(this.color);
boatPoly.setOnMouseEntered(event -> {
boatPoly.setFill(Color.FLORALWHITE);
boatPoly.setStroke(Color.RED);
});
boatPoly.setOnMouseExited(event -> {
boatPoly.setFill(color);
boatPoly.setFill(this.color);
boatPoly.setStroke(Color.BLACK);
});
boatPoly.setOnMouseClicked(event -> setIsSelected(!isSelected));
boatPoly.setCache(true);
boatPoly.setCacheHint(CacheHint.SPEED);
boatAnnotations = new BoatAnnotations(boat, color);
boatAnnotations = new BoatAnnotations(boat, this.color);
leftLayLine = new Line();
rightLayline = new Line();
@@ -160,42 +163,6 @@ public class BoatGroup extends Group {
boatPoly.getTransforms().setAll(new Rotate(rotation));
}
public void move() {
double dx = xIncrement * framesToMove;
double dy = yIncrement * framesToMove;
distanceTravelled += Math.abs(dx) + Math.abs(dy);
moveGroupBy(xIncrement, yIncrement);
framesToMove = framesToMove - 1;
if (framesToMove <= 0) {
isStopped = true;
}
// 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(boat.getColour());
// l.setCache(true);
// l.setCacheHint(CacheHint.SPEED);
// lineGroup.getChildren().add(l);
// }
//
// if (destinationSet) {
// lastPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY());
// }
// }
wake.updatePosition();
}
/**
* Sets the destination of the boat and the headng it should have once it reaches
*
@@ -206,26 +173,42 @@ public class BoatGroup extends Group {
*/
public void setDestination(double newXValue, double newYValue, double rotation,
double groundSpeed, long timeValid, double frameRate) {
if (lastTimeValid == 0) {
lastTimeValid = timeValid - 200;
moveTo(newXValue, newYValue, rotation);
}
framesToMove = Math.round((frameRate / (1000.0f / (timeValid - lastTimeValid))));
double dx = newXValue - boatPoly.getLayoutX();
double dy = newYValue - boatPoly.getLayoutY();
xIncrement = dx / framesToMove;
yIncrement = dy / framesToMove;
destinationSet = true;
Double dx = Math.abs(boatPoly.getLayoutX() - newXValue);
Double dy = Math.abs(boatPoly.getLayoutY() - newYValue);
moveTo(newXValue, newYValue, rotation);
rotateTo(rotation);
wake.setRotation(rotation, groundSpeed);
boat.setVelocity(groundSpeed);
lastTimeValid = timeValid;
isStopped = false;
lastRotation = rotation;
boatAnnotations.update();
distanceTravelled += Math.sqrt((dx * dx) + (dy * dy));
if (distanceTravelled > 10 && isPlayer) {
distanceTravelled = 0d;
if (lastPoint != null) {
Line l = new Line(
lastPoint.getX(),
lastPoint.getY(),
boatPoly.getLayoutX(),
boatPoly.getLayoutY()
);
l.getStrokeDashArray().setAll(3d, 7d);
l.setStroke(boat.getColour());
l.setCache(true);
l.setCacheHint(CacheHint.SPEED);
lineGroup.getChildren().add(l);
}
if (destinationSet) {
lastPoint = new Point2D(boatPoly.getLayoutX(), boatPoly.getLayoutY());
}
}
}
@@ -276,7 +259,7 @@ public class BoatGroup extends Group {
}
public void setVisibility (boolean teamName, boolean velocity, boolean estTime, boolean legTime, boolean trail, boolean wake) {
boatAnnotations.setVisibile(teamName, velocity, estTime, legTime);
boatAnnotations.setVisible(teamName, velocity, estTime, legTime);
this.wake.setVisible(wake);
this.lineGroup.setVisible(trail);
}
@@ -349,4 +332,18 @@ public class BoatGroup extends Group {
return boat.toString();
}
/**
* 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(3);
boatAnnotations.setAsPlayer();
isPlayer = true;
}
}
+22 -22
View File
@@ -55,29 +55,29 @@ public class Wake extends Group {
}
void setRotation (double rotation, double velocity) {
if (Math.abs(rotations[0] - rotation) > 20) {
// if (Math.abs(rotations[0] - rotation) > 20) {
rotate(rotation);
} 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];
}
}
}
// } 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) {
@@ -1,10 +1,11 @@
package seng302.gameServer;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import seng302.client.ClientPacketParser;
import seng302.models.Player;
import seng302.models.Yacht;
import seng302.server.messages.BoatActionType;
@@ -12,7 +13,9 @@ import seng302.server.messages.BoatActionType;
* A Static class to hold information about the current state of the game (model)
* Created by wmu16 on 10/07/17.
*/
public class GameState {
public class GameState implements Runnable {
private static Integer STATE_UPDATES_PER_SECOND = 60;
private static Long previousUpdateTime;
public static Double windDirection;
@@ -24,14 +27,17 @@ public class GameState {
private static Boolean isRaceStarted;
private static GameStages currentStage;
private static long startTime = System.currentTimeMillis();
public GameState(String hostIpAddress) {
windDirection = 170d;
windDirection = 180d;
windSpeed = 10000d;
yachts = new HashMap<>();
players = new ArrayList<>();
GameState.hostIpAddress = hostIpAddress;
this.hostIpAddress = hostIpAddress;
players = new ArrayList<>();
currentStage = GameStages.LOBBYING;
isRaceStarted = false;
@@ -39,6 +45,9 @@ public class GameState {
//set this when game stage changes to prerace
previousUpdateTime = System.currentTimeMillis();
yachts = new HashMap<>();
new Thread(this).start();
}
public static String getHostIpAddress() {
@@ -74,9 +83,17 @@ public class GameState {
}
public static void setCurrentStage(GameStages currentStage) {
if (currentStage == GameStages.RACING){
startTime = System.currentTimeMillis();
}
GameState.currentStage = currentStage;
}
public static long getStartTime(){
return startTime;
}
public static Double getWindDirection() {
return windDirection;
}
@@ -100,7 +117,6 @@ public class GameState {
case VMG:
playerYacht.turnToVMG();
// System.out.println("Snapping to VMG");
// TODO: 22/07/17 wmu16 - Add in the vmg calculation code here
break;
case SAILS_IN:
playerYacht.toggleSailIn();
@@ -134,7 +150,6 @@ public class GameState {
}
public static void update() {
Long timeInterval = System.currentTimeMillis() - previousUpdateTime;
previousUpdateTime = System.currentTimeMillis();
for (Yacht yacht : yachts.values()) {
@@ -151,4 +166,28 @@ public class GameState {
// TODO: 22/07/17 wmu16 - This may not be robust enough and may have to be improved on.
return yachts.size() + 1;
}
/**
* A thread to have the game state update itself at certain intervals
*/
@Override
public void run() {
while(true) {
try {
Thread.sleep(1000 / STATE_UPDATES_PER_SECOND);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (currentStage == GameStages.PRE_RACE) {
update();
}
//RACING
if (currentStage == GameStages.RACING) {
update();
}
}
}
}
@@ -11,28 +11,26 @@ import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.logging.Logger;
/**
* A class describing the overall server, which creates and collects server threads for each client
* Created by wmu16 on 13/07/17.
*/
public class MainServerThread extends Observable implements Runnable, PacketBufferDelegate, ClientConnectionDelegate{
public class MainServerThread extends Observable implements Runnable, ClientConnectionDelegate{
private static final int PORT = 4942;
private static final Integer MAX_NUM_PLAYERS = 3;
private static final Integer UPDATES_PER_SECOND = 2;
private static final Integer CLIENT_UPDATES_PER_SECOND = 10;
private static final int LOG_LEVEL = 1;
private Thread thread;
private ServerSocket serverSocket = null;
private Socket socket;
private ArrayList<ServerToClientThread> serverToClientThreads = new ArrayList<>();
private PriorityBlockingQueue<StreamPacket> packetBuffer;
public MainServerThread() {
try {
serverSocket = new ServerSocket(PORT);
@@ -40,8 +38,6 @@ public class MainServerThread extends Observable implements Runnable, PacketBuff
serverLog("IO error in server thread handler upon trying to make new server socket", 0);
}
packetBuffer = new PriorityBlockingQueue<>();
thread = new Thread(this);
thread.start();
}
@@ -57,22 +53,20 @@ public class MainServerThread extends Observable implements Runnable, PacketBuff
heartbeatThread.start();
serverListenThread.start();
//You should handle interrupts in some way, so that the thread won't keep on forever if you exit the app.
while (!thread.isInterrupted()) {
try {
Thread.sleep(1000 / UPDATES_PER_SECOND);
Thread.sleep(1000 / CLIENT_UPDATES_PER_SECOND);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (GameState.getCurrentStage() == GameStages.PRE_RACE) {
GameState.update();
updateClients();
}
//RACING
if (GameState.getCurrentStage() == GameStages.RACING) {
GameState.update();
updateClients();
}
@@ -81,14 +75,6 @@ public class MainServerThread extends Observable implements Runnable, PacketBuff
}
while (!packetBuffer.isEmpty()){
try {
StreamPacket packet = packetBuffer.take();
ClientPacketParser.parsePacket(packet);
} catch (InterruptedException e) {
continue;
}
}
}
// TODO: 14/07/17 wmu16 - Send out disconnect packet to clients
@@ -113,11 +99,6 @@ public class MainServerThread extends Observable implements Runnable, PacketBuff
}
}
@Override
public boolean addToBuffer(StreamPacket streamPacket) {
return packetBuffer.add(streamPacket);
}
/**
* A client has tried to connect to the server
* @param serverToClientThread The player that connected
@@ -155,8 +136,16 @@ public class MainServerThread extends Observable implements Runnable, PacketBuff
}
public void startGame() {
Timer t = new Timer();
t.schedule(new TimerTask() {
@Override
public void run() {
for (ServerToClientThread serverToClientThread : serverToClientThreads) {
serverToClientThread.sendRaceStatusMessage();
}
}
}, 0, 500);
}
}
@@ -1,9 +1,12 @@
package seng302.gameServer;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketException;
@@ -12,6 +15,9 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
import java.util.zip.CRC32;
import java.util.zip.Checksum;
@@ -64,17 +70,42 @@ public class ServerToClientThread implements Runnable, Observer {
public ServerToClientThread(Socket socket) {
this.socket = socket;
BufferedReader fn;
String fName = "";
BufferedReader ln;
String lName = "";
try {
is = socket.getInputStream();
os = socket.getOutputStream();
fn = new BufferedReader(
new InputStreamReader(
ServerToClientThread.class.getResourceAsStream(
"/server_config/CSV_Database_of_First_Names.csv"
)
)
);
List<String> all = fn.lines().collect(Collectors.toList());
fName = all.get(ThreadLocalRandom.current().nextInt(0, all.size()));
ln = new BufferedReader(
new InputStreamReader(
ServerToClientThread.class.getResourceAsStream(
"/server_config/CSV_Database_of_Last_Names.csv"
)
)
);
all = ln.lines().collect(Collectors.toList());
lName = all.get(ThreadLocalRandom.current().nextInt(0, all.size()));
} catch (IOException e) {
System.out.println("IO error in server thread upon grabbing streams");
e.printStackTrace();
}
//Attempt threeway handshake with connection
sourceId = GameState.getUniquePlayerID();
if (threeWayHandshake(sourceId)) {
serverLog("Successful handshake. Client allocated id: " + sourceId, 1);
Yacht yacht = new Yacht("Yacht", sourceId, sourceId.toString(), "Kapa", "Kappa", "NZ");
Yacht yacht = new Yacht(
"Yacht", sourceId, sourceId.toString(), fName, fName + " " + lName, "NZ"
);
// Yacht yacht = new Yacht("Kappa", "Kap", new GeoPoint(57.6708220, 11.8321340), 90.0);
GameState.addYacht(sourceId, yacht);
GameState.addPlayer(new Player(socket, yacht));
@@ -109,11 +140,6 @@ public class ServerToClientThread implements Runnable, Observer {
while (socket.isConnected()) {
try {
// if (initialisedRace) {
// sendSetupMessages();
// initialisedRace = false;
// }
//Perform a write if it is time to as delegated by the MainServerThread
if (updateClient) {
// TODO: 13/07/17 wmu16 - Write out game state - some function that would write all appropriate messages to this output stream
@@ -165,7 +191,6 @@ public class ServerToClientThread implements Runnable, Observer {
return;
}
}
}
private void sendSetupMessages() {
@@ -180,7 +205,8 @@ public class ServerToClientThread implements Runnable, Observer {
xml.setRegatta(new Regatta("RaceVision Test Game", 57.6679590, 11.8503233));
xml.setRace(race);
XMLMessage xmlMessage = new XMLMessage(xml.getRegattaAsXml(), XMLMessageSubType.REGATTA,
XMLMessage xmlMessage;
xmlMessage = new XMLMessage(xml.getRegattaAsXml(), XMLMessageSubType.REGATTA,
xml.getRegattaAsXml().length());
sendMessage(xmlMessage);
@@ -191,7 +217,6 @@ public class ServerToClientThread implements Runnable, Observer {
xmlMessage = new XMLMessage(xml.getRaceAsXml(), XMLMessageSubType.RACE,
xml.getRaceAsXml().length());
sendMessage(xmlMessage);
// System.out.println("Sent xml messages for " + thread.getName());
}
public void updateClient() {
@@ -310,8 +335,7 @@ public class ServerToClientThread implements Runnable, Observer {
public void sendRaceStatusMessage() {
// variables taken from GameServerThread
int TIME_TILL_RACE_START = 20 * 1000;
long startTime = System.currentTimeMillis() + TIME_TILL_RACE_START;
List<BoatSubMessage> boatSubMessages = new ArrayList<>();
BoatStatus boatStatus;
@@ -339,7 +363,7 @@ public class ServerToClientThread implements Runnable, Observer {
raceStatus = RaceStatus.WARNING;
}
sendMessage(new RaceStatusMessage(1, raceStatus, startTime, GameState.getWindDirection(),
sendMessage(new RaceStatusMessage(1, raceStatus, GameState.getStartTime(), GameState.getWindDirection(),
GameState.getWindSpeedMMS().longValue(), GameState.getPlayers().size(),
RaceType.MATCH_RACE, 1, boatSubMessages));
}
+1 -2
View File
@@ -4,8 +4,6 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
/**
@@ -182,4 +180,5 @@ public final class PolarTable {
return closestAngle;
}
}
+63 -13
View File
@@ -4,8 +4,7 @@ import static seng302.utilities.GeoUtility.getGeoCoordinate;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import javafx.scene.paint.Color;
import seng302.client.ClientPacketParser;
import seng302.controllers.RaceViewController;
@@ -119,19 +118,40 @@ public class Yacht {
* @param timeInterval since last update in milliseconds
*/
public void update(Long timeInterval) {
if (sailIn) {
Double secondsElapsed = timeInterval / 1000000.0;
Double windSpeedKnots = GameState.getWindSpeedKnots();
Double trueWindAngle = Math.abs(GameState.getWindDirection() - heading);
Double boatSpeedInKnots = PolarTable.getBoatSpeed(windSpeedKnots, trueWindAngle);
velocity = boatSpeedInKnots / ClientPacketParser.MS_TO_KNOTS * 1000;
Double metersCovered = velocity * secondsElapsed;
location = getGeoCoordinate(location, heading, metersCovered);
Double maxBoatSpeed = boatSpeedInKnots / ClientPacketParser.MS_TO_KNOTS * 1000;
if (sailIn && velocity <= maxBoatSpeed && maxBoatSpeed != 0d) {
if (velocity < maxBoatSpeed) {
velocity += maxBoatSpeed / 15; // Acceleration
}
if (velocity > maxBoatSpeed) {
velocity = maxBoatSpeed; // Prevent the boats from exceeding top speed
}
} else { // Deceleration
if (velocity > 0d) {
if (maxBoatSpeed != 0d) {
velocity -= maxBoatSpeed / 600;
} else {
velocity -= velocity / 100;
}
if (velocity < 0) {
velocity = 0d;
}
}
}
Double metersCovered = velocity * secondsElapsed;
location = getGeoCoordinate(location, heading, metersCovered);
}
public Double getHeading() {
return heading;
@@ -145,8 +165,7 @@ public class Yacht {
}
public void tackGybe(Double windDirection) {
Double normalizedHeading = heading - GameState.windDirection;
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360);
Double normalizedHeading = normalizeHeading();
adjustHeading(-2 * normalizedHeading);
}
@@ -155,8 +174,7 @@ public class Yacht {
}
public void turnUpwind() {
Double normalizedHeading = heading - GameState.windDirection;
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360);
Double normalizedHeading = normalizeHeading();
if (normalizedHeading == 0) {
if (lastHeading < 180) {
adjustHeading(-TURN_STEP);
@@ -177,8 +195,7 @@ public class Yacht {
}
public void turnDownwind() {
Double normalizedHeading = heading - GameState.windDirection;
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360);
Double normalizedHeading = normalizeHeading();
if (normalizedHeading == 0) {
if (lastHeading < 180) {
adjustHeading(TURN_STEP);
@@ -199,10 +216,43 @@ public class Yacht {
}
public void turnToVMG() {
// TODO: 25/07/17 wmu16 - Fix this so it grabs the optimal value from the optimal Polar
Double normalizedHeading = normalizeHeading();
Double optimalHeading;
HashMap<Double, Double> optimalPolarMap;
if (normalizedHeading >= 90 && normalizedHeading <= 270) { // Downwind
optimalPolarMap = PolarTable.getOptimalDownwindVMG(GameState.getWindSpeedKnots());
optimalHeading = optimalPolarMap.keySet().iterator().next();
} else {
optimalPolarMap = PolarTable.getOptimalUpwindVMG(GameState.getWindSpeedKnots());
optimalHeading = optimalPolarMap.keySet().iterator().next();
}
// Take optimal heading and turn into correct
optimalHeading =
optimalHeading + (double) Math.floorMod(GameState.getWindDirection().longValue(), 360L);
turnTowardsHeading(optimalHeading);
}
private void turnTowardsHeading(Double newHeading) {
System.out.println(newHeading);
if (heading < 90 && newHeading > 270) {
adjustHeading(-TURN_STEP);
} else {
if (heading < newHeading) {
adjustHeading(TURN_STEP);
} else {
adjustHeading(-TURN_STEP);
}
}
}
private Double normalizeHeading() {
Double normalizedHeading = heading - GameState.windDirection;
normalizedHeading = (double) Math.floorMod(normalizedHeading.longValue(), 360L);
return normalizedHeading;
}
public String getBoatType() {
return boatType;
+1 -1
View File
@@ -1,4 +1,4 @@
Tws,Twa0,Bsp0,Twa1,Bsp1,UpTwa,UpBsp,Twa2,Bsp2,Twa3,Bsp3,Twa4,Bsp4,Twa5,Bsp5,Twa6,Bsp6,DnTwa,DnBsp,Twa7,Bsp7
Tws,Twa0,Bsp0,Twa1,Bsp1,UpTwa,UpBsp,Twa2,Bsp2,Twa3,Bsp3,Twa4,Bsp4,Twa5,Bsp5,Twa6,Bsp6,DnTwa,DnBsp,Twa7,Bsp7
4,0,0,30,4,45,8,60,9,75,10,90,10,115,10,145,10,155,10,175,4
8,0,0,30,7,43,10,60,11,75,11,90,11,115,12,145,12,153,12,175,10
12,0,0,30,11,43,14.4,60,16,75,20,90,23,115,24,145,23,153,21.6,175,14
1 Tws Twa0 Bsp0 Twa1 Bsp1 UpTwa UpBsp Twa2 Bsp2 Twa3 Bsp3 Twa4 Bsp4 Twa5 Bsp5 Twa6 Bsp6 DnTwa DnBsp Twa7 Bsp7
2 4 0 0 30 4 45 8 60 9 75 10 90 10 115 10 145 10 155 10 175 4
3 8 0 0 30 7 43 10 60 11 75 11 90 11 115 12 145 12 153 12 175 10
4 12 0 0 30 11 43 14.4 60 16 75 20 90 23 115 24 145 23 153 21.6 175 14
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+10 -10
View File
@@ -20,31 +20,31 @@
<children>
<AnchorPane prefHeight="960.0" prefWidth="250.0" style="-fx-background-color: #2C2c36;" GridPane.rowSpan="3">
<children>
<Label layoutX="11.0" layoutY="259.0" text="Team Position" textFill="WHITE" />
<Label layoutX="11.0" layoutY="283.0" text="Team Position" textFill="WHITE" />
<Label layoutX="13.0" layoutY="432.0" text="Settings" textFill="WHITE" />
<Label layoutX="11.0" layoutY="14.0" text="Timer" textFill="WHITE" />
<Label layoutX="11.0" layoutY="88.0" text="Wind direction" textFill="WHITE" />
<Circle fx:id="windBackgroundCircle" blendMode="DARKEN" fill="#3dcdc8" layoutX="110.0" layoutY="166.0" radius="35.0" stroke="#d7d7d7" strokeType="INSIDE" strokeWidth="3.0" />
<Text fx:id="windArrowText" fill="#a8a8a8" layoutX="86.0" layoutY="186.0" strokeType="OUTSIDE" strokeWidth="0.0" text="↓">
<Label layoutX="11.0" layoutY="41.0" text="Timer" textFill="WHITE" />
<Label layoutX="11.0" layoutY="112.0" text="Wind direction" textFill="WHITE" />
<Circle fx:id="windBackgroundCircle" blendMode="DARKEN" fill="#3dcdc8" layoutX="110.0" layoutY="190.0" radius="35.0" stroke="#d7d7d7" strokeType="INSIDE" strokeWidth="3.0" />
<Text fx:id="windArrowText" fill="#a8a8a8" layoutX="86.0" layoutY="210.0" strokeType="OUTSIDE" strokeWidth="0.0" text="↓">
<font>
<Font name="AdobeArabic-Regular" size="55.0" />
</font>
</Text>
<Text fx:id="windDirectionText" fill="#d3d3d3" layoutX="171.0" layoutY="214.0" strokeType="OUTSIDE" strokeWidth="0.0" text="0.0°" textAlignment="RIGHT">
<Text fx:id="windDirectionText" fill="#d3d3d3" layoutX="171.0" layoutY="238.0" strokeType="OUTSIDE" strokeWidth="0.0" text="0.0°" textAlignment="RIGHT">
<font>
<Font name="System Bold" size="13.0" />
</font>
</Text>
<Text fx:id="windSpeedText" fill="#d3d3d3" layoutX="12.0" layoutY="213.0" strokeType="OUTSIDE" strokeWidth="0.0" text="0.0 Knot" textAlignment="RIGHT">
<Text fx:id="windSpeedText" fill="#d3d3d3" layoutX="12.0" layoutY="237.0" strokeType="OUTSIDE" strokeWidth="0.0" text="0.0 Knot" textAlignment="RIGHT">
<font>
<Font name="System Bold" size="13.0" />
</font>
</Text>
<CheckBox fx:id="toggleFps" focusTraversable="false" graphicTextGap="0.0" layoutX="21.0" layoutY="453.0" mnemonicParsing="false" prefHeight="18.0" prefWidth="143.0" selected="true" styleClass="ui-checkbox" text="Show FPS" textFill="WHITE" />
<VBox fx:id="positionVbox" layoutX="12.0" layoutY="280.0" prefHeight="140.0" prefWidth="200.0" styleClass="text-white" />
<Pane layoutX="11.0" layoutY="30.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="51.0" prefWidth="193.0">
<VBox fx:id="positionVbox" layoutX="12.0" layoutY="304.0" prefHeight="116.0" prefWidth="200.0" styleClass="text-white" />
<Pane layoutX="11.0" layoutY="39.0" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="51.0" prefWidth="193.0">
<children>
<Text fx:id="timerLabel" fill="#f8f8f8" layoutX="-26.0" layoutY="34.0" strokeType="OUTSIDE" strokeWidth="0.0" text="00:00" textAlignment="CENTER" wrappingWidth="246.0">
<Text fx:id="timerLabel" fill="#f8f8f8" layoutX="-26.0" layoutY="51.0" strokeType="OUTSIDE" strokeWidth="0.0" text="00:00" textAlignment="CENTER" wrappingWidth="246.0">
<font>
<Font size="25.0" />
</font>
@@ -24,4 +24,5 @@ public class YachtTest {
yachts.add(new Yacht("Yacht 3", "Y3", new GeoPoint(-35.0, -15.5), 20.0));
}
}