From af9f1417f1d4891542eeb0eb8c9e27e64beb6dfe Mon Sep 17 00:00:00 2001 From: Zhi You Tan Date: Wed, 26 Jul 2017 16:01:41 +1200 Subject: [PATCH 1/4] Documented client packet parser, client state, client state querying runnable, client to server thread and replaced e.printStackTrace() with client log messages. --- .../seng302/client/ClientPacketParser.java | 17 ++++-- src/main/java/seng302/client/ClientState.java | 13 ++--- .../client/ClientStateQueryingRunnable.java | 13 ++++- .../seng302/client/ClientToServerThread.java | 55 +++++++++++-------- 4 files changed, 61 insertions(+), 37 deletions(-) diff --git a/src/main/java/seng302/client/ClientPacketParser.java b/src/main/java/seng302/client/ClientPacketParser.java index 4198dc55..578344fa 100644 --- a/src/main/java/seng302/client/ClientPacketParser.java +++ b/src/main/java/seng302/client/ClientPacketParser.java @@ -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 @@ -108,7 +109,7 @@ public class ClientPacketParser { } } catch (NullPointerException e) { System.out.println("Error parsing packet"); - e.printStackTrace(); +// e.printStackTrace(); } } @@ -185,7 +186,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 +206,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,9 +217,12 @@ 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); } } @@ -286,8 +291,10 @@ 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; diff --git a/src/main/java/seng302/client/ClientState.java b/src/main/java/seng302/client/ClientState.java index 64512a1b..c96be561 100644 --- a/src/main/java/seng302/client/ClientState.java +++ b/src/main/java/seng302/client/ClientState.java @@ -1,8 +1,5 @@ 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; @@ -17,7 +14,7 @@ public class ClientState { private static Boolean raceStarted = false; private static Boolean connectedToHost = false; private static Map boats = new ConcurrentHashMap<>(); - private static Boolean dirtyState = true; + private static Boolean boatsUpdated = true; private static String clientSourceId = ""; public static String getHostIp() { @@ -56,12 +53,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() { diff --git a/src/main/java/seng302/client/ClientStateQueryingRunnable.java b/src/main/java/seng302/client/ClientStateQueryingRunnable.java index 67cf1dbf..576d7b24 100644 --- a/src/main/java/seng302/client/ClientStateQueryingRunnable.java +++ b/src/main/java/seng302/client/ClientStateQueryingRunnable.java @@ -12,6 +12,10 @@ public class ClientStateQueryingRunnable extends Observable implements Runnable public ClientStateQueryingRunnable() {} + /** + * Notifies observers "game started" if ClientState raceStarted flag is true and terminates itself. + * Notifies observers "update players" if ClientState boatsUpdated flag is true and resets the flag to false; + */ @Override public void run() { while(!terminate) { @@ -29,14 +33,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; } diff --git a/src/main/java/seng302/client/ClientToServerThread.java b/src/main/java/seng302/client/ClientToServerThread.java index 1e76bd07..1a8156b7 100644 --- a/src/main/java/seng302/client/ClientToServerThread.java +++ b/src/main/java/seng302/client/ClientToServerThread.java @@ -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 { @@ -30,8 +31,19 @@ public class ClientToServerThread implements Runnable { private OutputStream os; private Boolean updateClient = true; - private ByteArrayOutputStream crcBuffer; + 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,30 @@ public class ClientToServerThread implements Runnable { thread = new Thread(this); thread.start(); - } + /** + * Prints out log message and 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. Will exit 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(); @@ -101,7 +112,7 @@ public class ClientToServerThread implements Runnable { } } catch (Exception e) { closeSocket(); - e.printStackTrace(); + clientLog("Disconnected from server", 1); return; } } @@ -111,7 +122,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 if received * @return the sourceID allocated to us by the server */ private Integer threeWayHandshake() { @@ -120,14 +131,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 +155,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 +164,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 +175,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(); From ef6821a0cd38d7a8f02f06ca1b89729846eb8b9d Mon Sep 17 00:00:00 2001 From: Haoming Yin Date: Wed, 26 Jul 2017 19:55:35 +1200 Subject: [PATCH 2/4] Updated and added more documentations #story[1047] --- src/main/java/seng302/client/ClientState.java | 3 ++- .../client/ClientStateQueryingRunnable.java | 6 ++++-- .../seng302/client/ClientToServerThread.java | 8 ++++---- .../seng302/controllers/CanvasController.java | 16 ++++++++-------- .../seng302/controllers/LobbyController.java | 17 ++++++++++++++--- 5 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/main/java/seng302/client/ClientState.java b/src/main/java/seng302/client/ClientState.java index c96be561..053bc56f 100644 --- a/src/main/java/seng302/client/ClientState.java +++ b/src/main/java/seng302/client/ClientState.java @@ -5,7 +5,8 @@ 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 { diff --git a/src/main/java/seng302/client/ClientStateQueryingRunnable.java b/src/main/java/seng302/client/ClientStateQueryingRunnable.java index 576d7b24..23adccb0 100644 --- a/src/main/java/seng302/client/ClientStateQueryingRunnable.java +++ b/src/main/java/seng302/client/ClientStateQueryingRunnable.java @@ -13,8 +13,10 @@ public class ClientStateQueryingRunnable extends Observable implements Runnable public ClientStateQueryingRunnable() {} /** - * Notifies observers "game started" if ClientState raceStarted flag is true and terminates itself. - * Notifies observers "update players" if ClientState boatsUpdated flag is true and resets the flag to false; + * 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() { diff --git a/src/main/java/seng302/client/ClientToServerThread.java b/src/main/java/seng302/client/ClientToServerThread.java index 1a8156b7..da82dff0 100644 --- a/src/main/java/seng302/client/ClientToServerThread.java +++ b/src/main/java/seng302/client/ClientToServerThread.java @@ -65,7 +65,7 @@ public class ClientToServerThread implements Runnable { } /** - * Prints out log message and time happened. + * 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 @@ -78,7 +78,8 @@ public class ClientToServerThread implements Runnable { } /** - * Perform the thread loop. Will exit loop if ClientState connected to host variable is false. + * Perform the thread loop. It exits the loop if ClientState connected to host + * variable is false. */ public void run() { int sync1; @@ -104,7 +105,6 @@ 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); @@ -122,7 +122,7 @@ public class ClientToServerThread implements Runnable { /** - * Listens for an allocated sourceID and returns it to the server if received + * Listens for an allocated sourceID and returns it to the server * @return the sourceID allocated to us by the server */ private Integer threeWayHandshake() { diff --git a/src/main/java/seng302/controllers/CanvasController.java b/src/main/java/seng302/controllers/CanvasController.java index 06558675..eb2a56cb 100644 --- a/src/main/java/seng302/controllers/CanvasController.java +++ b/src/main/java/seng302/controllers/CanvasController.java @@ -74,7 +74,7 @@ public class CanvasController { private List markGroups = new ArrayList<>(); private List boatGroups = new ArrayList<>(); - private Text FPSdisplay = new Text(); + private Text FPSDisplay = new Text(); private Polygon raceBorder = new Polygon(); //FRAME RATE @@ -119,10 +119,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(); @@ -391,10 +391,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); } } diff --git a/src/main/java/seng302/controllers/LobbyController.java b/src/main/java/seng302/controllers/LobbyController.java index 56c38e37..9645c59f 100644 --- a/src/main/java/seng302/controllers/LobbyController.java +++ b/src/main/java/seng302/controllers/LobbyController.java @@ -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)); @@ -162,6 +173,9 @@ public class LobbyController implements Initializable, Observer{ } } + /** + * 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,14 +209,11 @@ public class LobbyController implements Initializable, Observer{ @FXML public void readyButtonPressed() { -// setContentPane("/views/RaceView.fxml"); GameState.setCurrentStage(GameStages.RACING); mainServerThread.startGame(); } - - private void switchToRaceView() { if (!switchedPane) { switchedPane = true; From 7db716f51c6b75b3178c4d2a88ed89e2c58d7791 Mon Sep 17 00:00:00 2001 From: Alistair McIntyre Date: Wed, 26 Jul 2017 20:12:57 +1200 Subject: [PATCH 3/4] Boat should move towards optimal angle upwind and downwind when pressing spacebar (VMG) #story[988] --- .../java/seng302/gameServer/GameState.java | 10 ++-- src/main/java/seng302/models/PolarTable.java | 3 +- src/main/java/seng302/models/Yacht.java | 49 +++++++++++++++---- src/main/resources/config/acc_polars.csv | 2 +- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/src/main/java/seng302/gameServer/GameState.java b/src/main/java/seng302/gameServer/GameState.java index abd46b51..e7f4010e 100644 --- a/src/main/java/seng302/gameServer/GameState.java +++ b/src/main/java/seng302/gameServer/GameState.java @@ -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; @@ -27,7 +28,7 @@ public class GameState implements Runnable { private static GameStages currentStage; public GameState(String hostIpAddress) { - windDirection = 170d; + windDirection = 180d; windSpeed = 10000d; yachts = new HashMap<>(); players = new ArrayList<>(); @@ -105,7 +106,6 @@ public class GameState implements Runnable { 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(); diff --git a/src/main/java/seng302/models/PolarTable.java b/src/main/java/seng302/models/PolarTable.java index 997b0356..396f1947 100644 --- a/src/main/java/seng302/models/PolarTable.java +++ b/src/main/java/seng302/models/PolarTable.java @@ -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; } + } \ No newline at end of file diff --git a/src/main/java/seng302/models/Yacht.java b/src/main/java/seng302/models/Yacht.java index df5a6b20..67ab3373 100644 --- a/src/main/java/seng302/models/Yacht.java +++ b/src/main/java/seng302/models/Yacht.java @@ -4,6 +4,7 @@ import static seng302.utilities.GeoUtility.getGeoCoordinate; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.HashMap; import javafx.scene.paint.Color; import seng302.client.ClientPacketParser; import seng302.controllers.RaceViewController; @@ -136,9 +137,9 @@ public class Yacht { if (velocity > 0d) { if (maxBoatSpeed != 0d) { - velocity -= maxBoatSpeed / 25; + velocity -= maxBoatSpeed / 600; } else { - velocity -= velocity / 25; + velocity -= velocity / 100; } if (velocity < 0) { velocity = 0d; @@ -164,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); } @@ -174,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); @@ -196,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); @@ -218,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 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; diff --git a/src/main/resources/config/acc_polars.csv b/src/main/resources/config/acc_polars.csv index ee7ea80e..c0860181 100644 --- a/src/main/resources/config/acc_polars.csv +++ b/src/main/resources/config/acc_polars.csv @@ -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 From 7917a2584bfd79bd52b62797f33728032abe1d78 Mon Sep 17 00:00:00 2001 From: Michael Rausch Date: Wed, 26 Jul 2017 20:28:13 +1200 Subject: [PATCH 4/4] Fixed alignment for wind direction --- src/main/resources/views/RaceView.fxml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/views/RaceView.fxml b/src/main/resources/views/RaceView.fxml index cbe3b2dd..eb152581 100644 --- a/src/main/resources/views/RaceView.fxml +++ b/src/main/resources/views/RaceView.fxml @@ -30,7 +30,7 @@ - +