diff --git a/.gitignore b/.gitignore
index 9e503182..ede3b67f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -92,6 +92,7 @@ nbactions.xml
.idea/misc.xml
.idea/compiler.xml
.idea/modules.xml
+.idea/codeStyleSettings.xml
# Sensitive or high-churn files:
.idea/dataSources.ids
diff --git a/racevisionGame/pom.xml b/racevisionGame/pom.xml
index f77e5ea3..b0f6d203 100644
--- a/racevisionGame/pom.xml
+++ b/racevisionGame/pom.xml
@@ -83,89 +83,40 @@
-
-
-
- mock
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.5.1
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 2.4.3
-
-
-
-
- visualiser.app.App
- ${maven.compiler.source}
- ${maven.compiler.target}
-
-
-
-
-
-
- package
-
- shade
-
-
-
-
-
-
-
-
-
-
- visualiser
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.5.1
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 2.4.3
-
-
-
-
- visualiser.app.App
- ${maven.compiler.source}
- ${maven.compiler.target}
-
-
-
-
-
-
- package
-
- shade
-
-
-
-
-
-
-
-
-
-
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.5.1
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 2.4.3
+
+
+
+
+ visualiser.app.App
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+
+
+
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
diff --git a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java
index 3584fbab..d399210d 100644
--- a/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java
+++ b/racevisionGame/src/main/java/mock/app/ConnectionAcceptor.java
@@ -1,18 +1,26 @@
package mock.app;
import mock.model.RaceLogic;
+import mock.model.ClientConnection;
+import mock.model.SourceIdAllocator;
+import mock.model.commandFactory.CompositeCommand;
import network.Messages.Enums.XMLMessageType;
import network.Messages.LatestMessages;
import network.Messages.XMLMessage;
-import visualiser.gameController.ControllerServer;
-import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
/**
* Connection acceptor for multiple clients
@@ -28,10 +36,31 @@ public class ConnectionAcceptor implements Runnable {
* Socket used to listen for clients on.
*/
private ServerSocket serverSocket;
- //mock outputs
- private ArrayBlockingQueue mockOutputList = new ArrayBlockingQueue<>(16, true);
- //latest messages
+
+
+ /**
+ * List of client connections.
+ */
+ private BlockingQueue clientConnections = new ArrayBlockingQueue<>(16, true);
+
+ /**
+ * Snapshot of the race.
+ */
private LatestMessages latestMessages;
+
+ /**
+ * Collection of commands from clients for race to execute.
+ */
+ private CompositeCommand compositeCommand;
+
+ /**
+ * Used to allocate source IDs to clients.
+ */
+ private SourceIdAllocator sourceIdAllocator;
+
+
+
+
//Acknowledgement number for packets
private int ackNumber = 0;
//race xml sequence number
@@ -40,22 +69,28 @@ public class ConnectionAcceptor implements Runnable {
private short boatXMLSequenceNumber;
//regatta xml sequence number
private short regattaXMLSequenceNumber;
- //controller server
- private ControllerServer controllerServer;
//
- private RaceLogic rl = null;
+ private RaceLogic raceLogic = null;
/**
* Connection Acceptor Constructor
* @param latestMessages Latest messages to be sent
+ * @param compositeCommand Collection of commands for race to execute.
+ * @param sourceIdAllocator Object used to allocate source IDs for clients.
+ * @param raceLogic The race the client will connect to.
* @throws IOException if a server socket cannot be instantiated.
*/
- public ConnectionAcceptor(LatestMessages latestMessages) throws IOException {
+ public ConnectionAcceptor(LatestMessages latestMessages, CompositeCommand compositeCommand, SourceIdAllocator sourceIdAllocator, RaceLogic raceLogic) throws IOException {
+
+ this.latestMessages = latestMessages;
+ this.compositeCommand = compositeCommand;
+ this.sourceIdAllocator = sourceIdAllocator;
+ this.raceLogic = raceLogic;
- this.latestMessages =latestMessages;
this.serverSocket = new ServerSocket(serverPort);
- CheckClientConnection checkClientConnection = new CheckClientConnection(mockOutputList);
- new Thread(checkClientConnection).start();
+ CheckClientConnection checkClientConnection = new CheckClientConnection(clientConnections);
+ new Thread(checkClientConnection, "ConnectionAcceptor()->CheckClientConnection thread").start();
+
}
public String getAddress() throws UnknownHostException {
@@ -67,28 +102,36 @@ public class ConnectionAcceptor implements Runnable {
}
- public void setRace(RaceLogic rl){
- this.rl = rl;
- }
/**
* Run the Acceptor
*/
@Override
public void run() {
- while(true){//should be connections not filled up
+
+ while(clientConnections.remainingCapacity() > 0) {
+
try {
- System.out.println("Waiting for a connection...");//TEMP DEBUG REMOVE
+
+
Socket mockSocket = serverSocket.accept();
- DataOutputStream outToVisualiser = new DataOutputStream(mockSocket.getOutputStream());
- MockOutput mockOutput = new MockOutput(latestMessages, outToVisualiser);
- this.controllerServer = new ControllerServer(mockSocket, rl);
- new Thread(mockOutput).start();
- new Thread(controllerServer).start();
- mockOutputList.add(mockOutput);
- System.out.println(String.format("%d number of Visualisers Connected.", mockOutputList.size()));
+
+ Logger.getGlobal().log(Level.INFO, String.format("Client connected. client ip/port = %s. Local ip/port = %s.", mockSocket.getRemoteSocketAddress(), mockSocket.getLocalSocketAddress()));
+
+
+ ClientConnection clientConnection = new ClientConnection(mockSocket, sourceIdAllocator, latestMessages, compositeCommand, raceLogic);
+
+ clientConnections.add(clientConnection);
+
+ new Thread(clientConnection, "ConnectionAcceptor.run()->ClientConnection thread " + clientConnection).start();
+
+
+
+ Logger.getGlobal().log(Level.INFO, String.format("%d number of Visualisers Connected.", clientConnections.size()));
+
} catch (IOException e) {
- e.printStackTrace();
+ Logger.getGlobal().log(Level.WARNING, "Got an IOException while a client was attempting to connect.", e);
+
}
}
@@ -99,14 +142,14 @@ public class ConnectionAcceptor implements Runnable {
*/
class CheckClientConnection implements Runnable{
- private ArrayBlockingQueue mocks;
+ private BlockingQueue connections;
/**
* Constructor
- * @param mocks Mocks "connected"
+ * @param connections Clients "connected"
*/
- public CheckClientConnection(ArrayBlockingQueue mocks){
- this.mocks = mocks;
+ public CheckClientConnection(BlockingQueue connections){
+ this.connections = connections;
}
/**
@@ -114,21 +157,44 @@ public class ConnectionAcceptor implements Runnable {
*/
@Override
public void run() {
- double timeSinceLastHeartBeat = System.currentTimeMillis();
- while(true) {
- //System.out.println(mocks.size());//used to see current amount of visualisers connected.
- ArrayBlockingQueue m = new ArrayBlockingQueue(16, true, mocks);
- for (MockOutput mo : m) {
- try {
- mo.sendHeartBeat();
- } catch (IOException e) {
- mocks.remove(mo);
+
+ //We track the number of times each connection fails the !isAlive() test.
+ //This is to give a bit of lee-way in case the connection checker checks a connection before its thread has actually started.
+ Map connectionDeadCount = new HashMap<>();
+
+ while(!Thread.interrupted()) {
+
+ //Make copy of connections.
+ List clientConnections = new ArrayList<>(connections);
+
+
+ for (ClientConnection client : clientConnections) {
+
+ connectionDeadCount.put(client, connectionDeadCount.getOrDefault(client, 0));
+
+ if (!client.isAlive()) {
+ //Add one to fail count.
+ connectionDeadCount.put(client, connectionDeadCount.get(client) + 1);
+ }
+
+ //We only remove them if they fail 5 times.
+ if (connectionDeadCount.get(client) > 5) {
+ connections.remove(client);
+ connectionDeadCount.remove(client);
+ client.terminate();
+
+ Logger.getGlobal().log(Level.WARNING, "CheckClientConnection is removing the dead connection: " + client);
}
}
+
try {
Thread.sleep(100);
+
} catch (InterruptedException e) {
- e.printStackTrace();
+ Logger.getGlobal().log(Level.WARNING, "CheckClientConnection was interrupted while sleeping.", e);
+ Thread.currentThread().interrupt();
+ return;
+
}
}
}
diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java
index 617ad584..0d95e1c1 100644
--- a/racevisionGame/src/main/java/mock/app/Event.java
+++ b/racevisionGame/src/main/java/mock/app/Event.java
@@ -1,9 +1,9 @@
package mock.app;
import mock.dataInput.PolarParser;
-import mock.model.MockRace;
-import mock.model.Polars;
-import mock.model.RaceLogic;
+import mock.exceptions.EventConstructionException;
+import mock.model.*;
+import mock.model.commandFactory.CompositeCommand;
import network.Messages.LatestMessages;
import shared.dataInput.*;
import shared.enums.XMLFileType;
@@ -11,22 +11,29 @@ import shared.exceptions.InvalidBoatDataException;
import shared.exceptions.InvalidRaceDataException;
import shared.exceptions.InvalidRegattaDataException;
import shared.exceptions.XMLReaderException;
+import shared.model.Bearing;
import shared.model.Constants;
import javax.xml.transform.TransformerException;
import java.io.IOException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
+import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
/**
* A Race Event, this holds all of the race's information as well as handling the connection to its clients.
*/
public class Event {
- private static Event theEvent = new Event();
+ /**
+ * Contents of the various xml files.
+ */
private String raceXML;
private String regattaXML;
private String boatXML;
@@ -35,68 +42,122 @@ public class Event {
private Polars boatPolars;
- private ConnectionAcceptor mockOutput;
+ /**
+ * Data sources containing data from the xml files.
+ */
+ RaceDataSource raceDataSource;
+ BoatDataSource boatDataSource;
+ RegattaDataSource regattaDataSource;
+
+
+ private ConnectionAcceptor connectionAcceptor;
private LatestMessages latestMessages;
+ private CompositeCommand compositeCommand;
+
+ /**
+ * This is used to allocate source IDs.
+ */
+ private SourceIdAllocator sourceIdAllocator;
+
+
+
+
+
/**
* Constructs an event, using various XML files.
+ * @param singlePlayer Whether or not to create a single player event.
+ * @throws EventConstructionException Thrown if we cannot create an Event for any reason.
*/
- private Event() {
- try {
- this.raceXML = getRaceXMLAtCurrentTime(XMLReader.readXMLFileToString("mock/mockXML/raceTest.xml", StandardCharsets.UTF_8));
- this.boatXML = XMLReader.readXMLFileToString("mock/mockXML/boatsSinglePlayer.xml", StandardCharsets.UTF_8);
- this.regattaXML = XMLReader.readXMLFileToString("mock/mockXML/regattaTest.xml", StandardCharsets.UTF_8);
- this.xmlFileType = XMLFileType.Contents;
+ public Event(boolean singlePlayer) throws EventConstructionException {
- this.boatPolars = PolarParser.parse("mock/polars/acc_polars.csv");
+ String raceXMLFile = "mock/mockXML/raceTest.xml";
+ String boatsXMLFile = "mock/mockXML/boatTest.xml";
+ String regattaXMLFile = "mock/mockXML/regattaTest.xml";
- this.latestMessages = new LatestMessages();
- this.mockOutput = new ConnectionAcceptor(latestMessages);
+ if (singlePlayer) {
+ raceXMLFile = "mock/mockXML/raceSinglePlayer.xml";
+ boatsXMLFile = "mock/mockXML/boatsSinglePlayer.xml";
}
- catch (IOException e) {
- e.printStackTrace();
+
+ //Read XML files.
+ try {
+ this.raceXML = getRaceXMLAtCurrentTime(XMLReader.readXMLFileToString(raceXMLFile, StandardCharsets.UTF_8));
+ this.boatXML = XMLReader.readXMLFileToString(boatsXMLFile, StandardCharsets.UTF_8);
+ this.regattaXML = XMLReader.readXMLFileToString(regattaXMLFile, StandardCharsets.UTF_8);
+
} catch (XMLReaderException e) {
- e.printStackTrace();
- } catch (TransformerException e) {
- e.printStackTrace();
+ throw new EventConstructionException("Could not read XML files.", e);
}
- }
- public static Event getEvent() {
- return theEvent;
- }
+ this.xmlFileType = XMLFileType.Contents;
- public String getAddress() throws UnknownHostException {
- return mockOutput.getAddress();
- }
+ this.boatPolars = PolarParser.parse("mock/polars/acc_polars.csv");
- public int getPort() {
- return mockOutput.getServerPort();
- }
- /**
- * Sends the initial race data and then begins race simulation.
- * @throws InvalidRaceDataException Thrown if the race xml file cannot be parsed.
- * @throws XMLReaderException Thrown if any of the xml files cannot be parsed.
- * @throws InvalidBoatDataException Thrown if the boat xml file cannot be parsed.
- * @throws InvalidRegattaDataException Thrown if the regatta xml file cannot be parsed.
- */
- public void start() throws InvalidRaceDataException, XMLReaderException, InvalidBoatDataException, InvalidRegattaDataException {
- new Thread(mockOutput).start();
+ //Parse the XML files into data sources.
+ try {
+ this.raceDataSource = new RaceXMLReader(this.raceXML, this.xmlFileType);
+ this.boatDataSource = new BoatXMLReader(this.boatXML, this.xmlFileType);
+ this.regattaDataSource = new RegattaXMLReader(this.regattaXML, this.xmlFileType);
- sendXMLs();
- //Parse the XML files into data sources.
- RaceDataSource raceDataSource = new RaceXMLReader(this.raceXML, this.xmlFileType);
- BoatDataSource boatDataSource = new BoatXMLReader(this.boatXML, this.xmlFileType);
- RegattaDataSource regattaDataSource = new RegattaXMLReader(this.regattaXML, this.xmlFileType);
+ } catch (XMLReaderException | InvalidRaceDataException | InvalidRegattaDataException | InvalidBoatDataException e) {
+ throw new EventConstructionException("Could not parse XML files.", e);
+
+ }
+
+ this.sourceIdAllocator = new SourceIdAllocator(raceDataSource.getParticipants());
+ this.compositeCommand = new CompositeCommand();
+ this.latestMessages = new LatestMessages();
//Create and start race.
- RaceLogic newRace = new RaceLogic(new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.latestMessages, this.boatPolars, Constants.RaceTimeScale), this.latestMessages);
+ WindGenerator windGenerator = new RandomWindGenerator(
+ Bearing.fromDegrees(225),
+ Bearing.fromDegrees(215),
+ Bearing.fromDegrees(235),
+ 12d,
+ 8d,
+ 16d );
+ RaceLogic newRace = new RaceLogic(
+ new MockRace(
+ boatDataSource,
+ raceDataSource,
+ regattaDataSource,
+ this.boatPolars,
+ Constants.RaceTimeScale,
+ windGenerator ),
+ this.latestMessages,
+ this.compositeCommand);
+
+ new Thread(newRace, "Event.Start()->RaceLogic thread").start();
+
+
+ //Create connection acceptor.
+
+ try {
+ this.connectionAcceptor = new ConnectionAcceptor(latestMessages, compositeCommand, sourceIdAllocator, newRace);
+
+ } catch (IOException e) {
+ throw new EventConstructionException("Could not create ConnectionAcceptor.", e);
+ }
+
+
+ new Thread(connectionAcceptor, "Event.Start()->ConnectionAcceptor thread").start();
+
+ sendXMLs();
+
+
+ }
+
+
+
+ /**
+ * Sends the initial race data and then begins race simulation.
+ */
+ public void start() {
- mockOutput.setRace(newRace);
- new Thread(newRace).start();
}
/**
@@ -104,11 +165,11 @@ public class Event {
*/
private void sendXMLs() {
- mockOutput.setRegattaXml(regattaXML);
+ connectionAcceptor.setRegattaXml(regattaXML);
- mockOutput.setRaceXml(raceXML);
+ connectionAcceptor.setRaceXml(raceXML);
- mockOutput.setBoatsXml(boatXML);
+ connectionAcceptor.setBoatsXml(boatXML);
}
/**
@@ -119,7 +180,7 @@ public class Event {
private String getRaceXMLAtCurrentTime(String raceXML) {
//The start time is current time + 4 minutes. prestart is 3 minutes, and we add another minute.
- long millisecondsToAdd = Constants.RacePreStartTime + (1 * 60 * 1000);
+ long millisecondsToAdd = Constants.RacePreStartTime + Duration.ofMinutes(1).toMillis();
long secondsToAdd = millisecondsToAdd / 1000;
//Scale the time using our time scalar.
secondsToAdd = secondsToAdd / Constants.RaceTimeScale;
diff --git a/racevisionGame/src/main/java/mock/app/MockOutput.java b/racevisionGame/src/main/java/mock/app/MockOutput.java
index aff0947e..9536507b 100644
--- a/racevisionGame/src/main/java/mock/app/MockOutput.java
+++ b/racevisionGame/src/main/java/mock/app/MockOutput.java
@@ -2,37 +2,25 @@ package mock.app;
-import network.BinaryMessageEncoder;
-import network.MessageEncoders.RaceVisionByteEncoder;
import network.Messages.*;
-import network.Messages.Enums.MessageType;
-import network.Messages.Enums.XMLMessageType;
+import shared.model.RunnableWithFramePeriod;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.SocketException;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
/**
* TCP server to send race information to connected clients.
*/
-public class MockOutput implements Runnable
-{
- /**
- * Timestamp of the last sent heartbeat message.
- */
- private long lastHeartbeatTime;
+public class MockOutput implements RunnableWithFramePeriod {
+
- /**
- * Period for the heartbeat - that is, how often we send it.
- */
- private double heartbeatPeriod = 5.0;
/**
- * Output stream which wraps around mockSocket outstream.
+ * A queue to send messages to client.
*/
- private DataOutputStream outToVisualiser;
+ private BlockingQueue outgoingMessages;
/**
@@ -43,282 +31,83 @@ public class MockOutput implements Runnable
- /**
- * Ack numbers used in messages.
- */
- private int ackNumber = 1;
- /**
- * Sequence number for heartbeat messages.
- */
- private int heartbeatSequenceNum = 1;
- private boolean stop = false; //whether or not hte thread keeps running
-
/**
* Ctor.
- * @param latestMessages Latests Messages that the Mock is to send out
- * @param outToVisualiser DataStream to output to Visualisers
- * @throws IOException if server socket cannot be opened.
+ * @param latestMessages Latest Messages that the Mock is to send out
+ * @param outgoingMessages A queue to place outgoing messages on.
*/
- public MockOutput(LatestMessages latestMessages, DataOutputStream outToVisualiser) throws IOException {
-
- this.outToVisualiser = outToVisualiser;
-
- this.lastHeartbeatTime = System.currentTimeMillis();
-
+ public MockOutput(LatestMessages latestMessages, BlockingQueue outgoingMessages) {
+ this.outgoingMessages = outgoingMessages;
this.latestMessages = latestMessages;
-
- }
-
-
- /**
- * Increments the ackNumber value, and returns it.
- * @return Incremented ackNumber.
- */
- private int getNextAckNumber(){
- this.ackNumber++;
-
- return this.ackNumber;
- }
-
-
- /**
- * Calculates the time since last heartbeat message, in seconds.
- * @return Time since last heartbeat message, in seconds.
- */
- private double timeSinceHeartbeat() {
- long now = System.currentTimeMillis();
- return (now - lastHeartbeatTime) / 1000.0;
}
- /**
- * Generates the next heartbeat message and returns it. Increments the heartbeat sequence number.
- * @return The next heartbeat message.
- */
- private Heartbeat createHeartbeatMessage() {
-
- //Create the heartbeat message.
- Heartbeat heartbeat = new Heartbeat(this.heartbeatSequenceNum);
- heartbeatSequenceNum++;
-
- return heartbeat;
- }
-
- /**
- * Serializes a heartbeat message into a packet to be sent, and returns the byte array.
- * @param heartbeat The heartbeat message to serialize.
- * @return Byte array containing the next heartbeat message.
- */
- private byte[] parseHeartbeat(Heartbeat heartbeat) {
-
- //Serializes the heartbeat message.
- byte[] heartbeatMessage = RaceVisionByteEncoder.heartBeat(heartbeat);
-
- //Places the serialized message in a packet.
- BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(
- MessageType.HEARTBEAT,
- System.currentTimeMillis(),
- getNextAckNumber(),
- (short) heartbeatMessage.length,
- heartbeatMessage );
-
- return binaryMessageEncoder.getFullMessage();
-
- }
-
- /**
- * Encodes/serialises a XMLMessage message, and returns it.
- * @param xmlMessage The XMLMessage message to serialise.
- * @return The XMLMessage message in a serialised form.
- */
- private synchronized byte[] parseXMLMessage(XMLMessage xmlMessage) {
-
- //Serialize the xml message.
- byte[] encodedXML = RaceVisionByteEncoder.xmlMessage(xmlMessage);
-
- //Place the message in a packet.
- BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(
- MessageType.XMLMESSAGE,
- System.currentTimeMillis(),
- xmlMessage.getAckNumber(), //We use the ack number from the xml message.
- (short) encodedXML.length,
- encodedXML );
-
-
- return binaryMessageEncoder.getFullMessage();
-
- }
-
-
-
- /**
- * Encodes/serialises a BoatLocation message, and returns it.
- * @param boatLocation The BoatLocation message to serialise.
- * @return The BoatLocation message in a serialised form.
- */
- private synchronized byte[] parseBoatLocation(BoatLocation boatLocation){
-
-
- //Encodes the message.
- byte[] encodedBoatLoc = RaceVisionByteEncoder.boatLocation(boatLocation);
-
- //Encodes the full message with header.
- BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(
- MessageType.BOATLOCATION,
- System.currentTimeMillis(),
- getNextAckNumber(),
- (short) encodedBoatLoc.length,
- encodedBoatLoc );
-
-
- return binaryMessageEncoder.getFullMessage();
-
- }
-
- /**
- * Encodes/serialises a RaceStatus message, and returns it.
- * @param raceStatus The RaceStatus message to serialise.
- * @return The RaceStatus message in a serialised form.
- */
- private synchronized byte[] parseRaceStatus(RaceStatus raceStatus){
-
- //Encodes the messages.
- byte[] encodedRaceStatus = RaceVisionByteEncoder.raceStatus(raceStatus);
-
- //Encodes the full message with header.
- BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(
- MessageType.RACESTATUS,
- System.currentTimeMillis(),
- getNextAckNumber(),
- (short) encodedRaceStatus.length,
- encodedRaceStatus );
-
-
- return binaryMessageEncoder.getFullMessage();
-
-
- }
-
- /**
- * Sends a heartbeat
- * @throws IOException if the socket is no longer open at both ends the heartbeat returns an error.
- */
- public void sendHeartBeat() throws IOException {
- //Sends a heartbeat every so often.
- if (timeSinceHeartbeat() >= heartbeatPeriod) {
- outToVisualiser.write(parseHeartbeat(createHeartbeatMessage()));
- lastHeartbeatTime = System.currentTimeMillis();
- }
- }
/**
* Sending loop of the Server
*/
public void run() {
- try {
- while (!stop){
-
- //Wait until all of the xml files have been set.
- if (!this.latestMessages.hasAllXMLMessages()) {
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- continue;
- }
-
-
- long previousFrameTime = System.currentTimeMillis();
- boolean sentXMLs = false;
+ //Wait until all of the xml files have been set.
+ while (!this.latestMessages.hasAllXMLMessages()) {
- while(true) {
- try {
+ try {
+ Thread.sleep(500);
- long currentFrameTime = System.currentTimeMillis();
+ } catch (InterruptedException e) {
+ //If we get interrupted, exit the function.
+ Logger.getGlobal().log(Level.WARNING, "MockOutput.run().sleep(waitForXMLs) was interrupted on thread: " + Thread.currentThread(), e);
- //This is the time elapsed, in milliseconds, since the last server "frame".
- long framePeriod = currentFrameTime - previousFrameTime;
+ //Re-set the interrupt flag.
+ Thread.currentThread().interrupt();
+ return;
- //We only attempt to send packets every X milliseconds.
- long minimumFramePeriod = 16;
- if (framePeriod >= minimumFramePeriod) {
-
- //Send XML messages.
- if (!sentXMLs) {
- //Serialise them.
- byte[] raceXMLBlob = parseXMLMessage(latestMessages.getRaceXMLMessage());
- byte[] regattaXMLBlob = parseXMLMessage(latestMessages.getRegattaXMLMessage());
- byte[] boatsXMLBlob = parseXMLMessage(latestMessages.getBoatXMLMessage());
-
- //Send them.
- outToVisualiser.write(raceXMLBlob);
- outToVisualiser.write(regattaXMLBlob);
- outToVisualiser.write(boatsXMLBlob);
- sentXMLs = true;
- }
-
- //Sends the RaceStatus message.
- if (this.latestMessages.getRaceStatus() != null) {
- byte[] raceStatusBlob = this.parseRaceStatus(this.latestMessages.getRaceStatus());
-
- this.outToVisualiser.write(raceStatusBlob);
- }
+ }
+ }
- //Send all of the BoatLocation messages.
- for (int sourceID : this.latestMessages.getBoatLocationMap().keySet()) {
- //Get the message.
- BoatLocation boatLocation = this.latestMessages.getBoatLocation(sourceID);
- if (boatLocation != null) {
+ long previousFrameTime = System.currentTimeMillis();
+ boolean sentXMLs = false;
- //Encode.
- byte[] boatLocationBlob = this.parseBoatLocation(boatLocation);
- //Write it.
- this.outToVisualiser.write(boatLocationBlob);
- }
- }
+ while (!Thread.interrupted()) {
- previousFrameTime = currentFrameTime;
+ try {
+ long currentFrameTime = System.currentTimeMillis();
+ waitForFramePeriod(previousFrameTime, currentFrameTime, 16);
+ previousFrameTime = currentFrameTime;
- } else {
- //Wait until the frame period will be large enough.
- long timeToWait = minimumFramePeriod - framePeriod;
- try {
- Thread.sleep(timeToWait);
- } catch (InterruptedException e) {
- //If we get interrupted, exit the function.
- e.printStackTrace();
- //Re-set the interrupt flag.
- Thread.currentThread().interrupt();
- return;
- }
+ //Send XML messages.
+ if (!sentXMLs) {
- }
+ outgoingMessages.put(latestMessages.getRaceXMLMessage());
+ outgoingMessages.put(latestMessages.getRegattaXMLMessage());
+ outgoingMessages.put(latestMessages.getBoatXMLMessage());
- } catch (SocketException e) {
- break;
- }
+ sentXMLs = true;
+ }
+ List snapshot = latestMessages.getSnapshot();
+ for (AC35Data message : snapshot) {
+ outgoingMessages.put(message);
}
+
+ } catch (InterruptedException e) {
+ Logger.getGlobal().log(Level.WARNING, "MockOutput.run() interrupted while putting message in queue.", e);
+ Thread.currentThread().interrupt();
+ return;
}
- } catch (IOException e) {
- e.printStackTrace();
}
- }
- public void stop(){
- stop = true;
}
+
}
diff --git a/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java b/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java
new file mode 100644
index 00000000..79817ef9
--- /dev/null
+++ b/racevisionGame/src/main/java/mock/enums/ConnectionStateEnum.java
@@ -0,0 +1,95 @@
+package mock.enums;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The states in which a connection to a client may have.
+ */
+public enum ConnectionStateEnum {
+
+ UNKNOWN(0),
+
+ /**
+ * We're waiting for the client to complete the joining handshake (see {@link network.Messages.RequestToJoin}.
+ */
+ WAITING_FOR_HANDSHAKE(1),
+
+ /**
+ * The server has receved a {@link network.Messages.RequestToJoin} from the client.
+ */
+ REQUEST_RECEIVED(2),
+
+ /**
+ * The client has completed the handshake, and is connected.
+ * That is, the client sent a {@link network.Messages.RequestToJoin}, which was successful, and the server responded with a {@link network.Messages.JoinAcceptance}.
+ */
+ CONNECTED(3),
+
+ /**
+ * The client has timed out.
+ */
+ TIMED_OUT(4),
+
+ /**
+ * The client's connection has been declined.
+ */
+ DECLINED(5);
+
+
+
+
+ private byte value;
+
+ /**
+ * Ctor. Creates a ConnectionStateEnum from a given primitive integer value, cast to a byte.
+ * @param value Integer, which is cast to byte, to construct from.
+ */
+ private ConnectionStateEnum(int value) {
+ this.value = (byte) value;
+ }
+
+ /**
+ * Returns the primitive value of the enum.
+ * @return Primitive value of the enum.
+ */
+ public byte getValue() {
+ return value;
+ }
+
+
+ /**
+ * Stores a mapping between Byte values and ConnectionStateEnum values.
+ */
+ private static final Map byteToStatusMap = new HashMap<>();
+
+
+ /*
+ Static initialization block. Initializes the byteToStatusMap.
+ */
+ static {
+ for (ConnectionStateEnum type : ConnectionStateEnum.values()) {
+ ConnectionStateEnum.byteToStatusMap.put(type.value, type);
+ }
+ }
+
+
+ /**
+ * Returns the enumeration value which corresponds to a given byte value.
+ * @param connectionState Byte value to convert to a ConnectionStateEnum value.
+ * @return The ConnectionStateEnum value which corresponds to the given byte value.
+ */
+ public static ConnectionStateEnum fromByte(byte connectionState) {
+ //Gets the corresponding MessageType from the map.
+ ConnectionStateEnum type = ConnectionStateEnum.byteToStatusMap.get(connectionState);
+
+ if (type == null) {
+ //If the byte value wasn't found, return the UNKNOWN connectionState.
+ return ConnectionStateEnum.UNKNOWN;
+ } else {
+ //Otherwise, return the connectionState.
+ return type;
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/mock/exceptions/CommandConstructionException.java b/racevisionGame/src/main/java/mock/exceptions/CommandConstructionException.java
new file mode 100644
index 00000000..f1b286f7
--- /dev/null
+++ b/racevisionGame/src/main/java/mock/exceptions/CommandConstructionException.java
@@ -0,0 +1,24 @@
+package mock.exceptions;
+
+/**
+ * An exception thrown when we cannot create a command for some reasn (e.g., uknown action type).
+ */
+public class CommandConstructionException extends Exception {
+
+ /**
+ * Constructs the exception with a given message.
+ * @param message Message to store.
+ */
+ public CommandConstructionException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs the exception with a given message and cause.
+ * @param message Message to store.
+ * @param cause Cause to store.
+ */
+ public CommandConstructionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/racevisionGame/src/main/java/mock/exceptions/EventConstructionException.java b/racevisionGame/src/main/java/mock/exceptions/EventConstructionException.java
new file mode 100644
index 00000000..0f1d9b9f
--- /dev/null
+++ b/racevisionGame/src/main/java/mock/exceptions/EventConstructionException.java
@@ -0,0 +1,24 @@
+package mock.exceptions;
+
+/**
+ * An exception thrown when we cannot create an {@link mock.app.Event}.
+ */
+public class EventConstructionException extends Exception {
+
+ /**
+ * Constructs the exception with a given message.
+ * @param message Message to store.
+ */
+ public EventConstructionException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs the exception with a given message and cause.
+ * @param message Message to store.
+ * @param cause Cause to store.
+ */
+ public EventConstructionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/racevisionGame/src/main/java/mock/exceptions/SourceIDAllocationException.java b/racevisionGame/src/main/java/mock/exceptions/SourceIDAllocationException.java
new file mode 100644
index 00000000..6623d9cb
--- /dev/null
+++ b/racevisionGame/src/main/java/mock/exceptions/SourceIDAllocationException.java
@@ -0,0 +1,24 @@
+package mock.exceptions;
+
+/**
+ * An exception thrown when we cannot allocate a source ID.
+ */
+public class SourceIDAllocationException extends Exception {
+
+ /**
+ * Constructs the exception with a given message.
+ * @param message Message to store.
+ */
+ public SourceIDAllocationException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs the exception with a given message and cause.
+ * @param message Message to store.
+ * @param cause Cause to store.
+ */
+ public SourceIDAllocationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/racevisionGame/src/main/java/mock/model/ClientConnection.java b/racevisionGame/src/main/java/mock/model/ClientConnection.java
new file mode 100644
index 00000000..d2ca7609
--- /dev/null
+++ b/racevisionGame/src/main/java/mock/model/ClientConnection.java
@@ -0,0 +1,288 @@
+package mock.model;
+
+
+import mock.app.MockOutput;
+import mock.enums.ConnectionStateEnum;
+import shared.exceptions.HandshakeException;
+import mock.exceptions.SourceIDAllocationException;
+import mock.model.commandFactory.CompositeCommand;
+import network.Messages.*;
+import network.Messages.Enums.JoinAcceptanceEnum;
+import network.Messages.Enums.MessageType;
+import network.Messages.Enums.RequestToJoinEnum;
+import network.StreamRelated.MessageDeserialiser;
+import network.StreamRelated.MessageSerialiser;
+import visualiser.gameController.ControllerServer;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class handles the client connection handshake, and creation of MockOutput and ControllerServer.
+ */
+public class ClientConnection implements Runnable {
+
+ /**
+ * The socket for the client's connection.
+ */
+ private Socket socket;
+
+ /**
+ * Periodically sends HeartBeat messages to client.
+ */
+ private HeartBeatService heartBeatService;
+
+ /**
+ * The thread the {@link HeartBeatService} runs on.
+ */
+ private Thread heartBeatThread;
+
+
+ /**
+ * Used to allocate source ID to client, if they request to participate.
+ */
+ private SourceIdAllocator sourceIdAllocator;
+
+ /**
+ * Latest snapshot of the race, to send to client. Currently only used for XML messages.
+ */
+ private LatestMessages latestMessages;
+
+
+ /**
+ * Collection of commands from client for race to execute.
+ */
+ private CompositeCommand compositeCommand;
+
+ /**
+ * The race the client is connected to.
+ */
+ private RaceLogic raceLogic;
+
+ /**
+ * Used to send the race snapshot to client.
+ */
+ private MockOutput mockOutput;
+
+ /**
+ * The thread the {@link MockOutput} runs on.
+ */
+ private Thread mockOutputThread;
+
+ /**
+ * Used to receive client input, and turn it into commands.
+ */
+ private ControllerServer controllerServer;
+
+ /**
+ * The thread the {@link ControllerServer} runs on.
+ */
+ private Thread controllerServerThread;
+
+
+ /**
+ * Used to write messages to socket.
+ */
+ private MessageSerialiser messageSerialiser;
+
+ /**
+ * Stores messages to write to socket.
+ */
+ private BlockingQueue outputQueue;
+
+ /**
+ * Used to read messages from socket.
+ */
+ private MessageDeserialiser messageDeserialiser;
+
+ /**
+ * Stores messages read from socket.
+ */
+ private BlockingQueue inputQueue;
+
+ /**
+ * The state of the connection to the client.
+ */
+ private ConnectionStateEnum connectionState = ConnectionStateEnum.UNKNOWN;
+
+
+
+
+
+
+ /**
+ * Creates a client connection, using a given socket.
+ * @param socket The socket which connects to the client.
+ * @param sourceIdAllocator Used to allocate a source ID for the client.
+ * @param latestMessages Latest race snapshot to send to client.
+ * @param compositeCommand Collection of commands for race to execute.
+ * @param raceLogic The race the client is connected to.
+ * @throws IOException Thrown if there is a problem with the client socket.
+ */
+ public ClientConnection(Socket socket, SourceIdAllocator sourceIdAllocator, LatestMessages latestMessages, CompositeCommand compositeCommand, RaceLogic raceLogic) throws IOException {
+ this.socket = socket;
+ this.sourceIdAllocator = sourceIdAllocator;
+ this.latestMessages = latestMessages;
+ this.compositeCommand = compositeCommand;
+ this.raceLogic = raceLogic;
+
+ this.outputQueue = new LinkedBlockingQueue<>();
+ this.inputQueue = new LinkedBlockingQueue<>();
+
+
+ this.messageSerialiser = new MessageSerialiser(socket.getOutputStream(), outputQueue);
+ this.messageDeserialiser = new MessageDeserialiser(socket.getInputStream(), inputQueue);
+
+ new Thread(messageSerialiser, "ClientConnection()->MessageSerialiser thread " + messageSerialiser).start();
+ new Thread(messageDeserialiser, "ClientConnection()->MessageDeserialiser thread " + messageDeserialiser).start();
+
+
+ this.heartBeatService = new HeartBeatService(outputQueue);
+ this.heartBeatThread = new Thread(heartBeatService, "ClientConnection()->HeartBeatService thread " + heartBeatService);
+ this.heartBeatThread.start();
+
+ }
+
+
+
+ @Override
+ public void run() {
+ try {
+ handshake();
+
+ } catch (HandshakeException | SourceIDAllocationException e) {
+ Logger.getGlobal().log(Level.WARNING, "Client handshake failed.", e);
+ Thread.currentThread().interrupt();
+ return;
+ }
+
+ }
+
+
+ /**
+ * Initiates the handshake with the client.
+ * @throws HandshakeException Thrown if something goes wrong with the handshake.
+ * @throws SourceIDAllocationException Thrown if we cannot allocate a sourceID.
+ */
+ private void handshake() throws SourceIDAllocationException, HandshakeException {
+
+ //This function is a bit messy, and could probably be refactored a bit.
+
+ connectionState = ConnectionStateEnum.WAITING_FOR_HANDSHAKE;
+
+
+
+ RequestToJoin requestToJoin = waitForRequestToJoin();
+
+ int allocatedSourceID = 0;
+
+ //If they want to participate, give them a source ID number.
+ if (requestToJoin.getRequestType() == RequestToJoinEnum.PARTICIPANT) {
+
+ allocatedSourceID = sourceIdAllocator.allocateSourceID();
+
+ this.controllerServer = new ControllerServer(compositeCommand, inputQueue, allocatedSourceID, raceLogic.getRace());
+ this.controllerServerThread = new Thread(controllerServer, "ClientConnection.run()->ControllerServer thread" + controllerServer);
+ this.controllerServerThread.start();
+
+ }
+
+
+ sendJoinAcceptanceMessage(allocatedSourceID);
+
+ this.mockOutput = new MockOutput(latestMessages, outputQueue);
+ this.mockOutputThread = new Thread(mockOutput, "ClientConnection.run()->MockOutput thread" + mockOutput);
+ this.mockOutputThread.start();
+
+
+ connectionState = ConnectionStateEnum.CONNECTED;
+
+ }
+
+
+ /**
+ * Waits until the client sends a {@link RequestToJoin} message, and returns it.
+ * @return The {@link RequestToJoin} message.
+ * @throws HandshakeException Thrown if we get interrupted while waiting.
+ */
+ private RequestToJoin waitForRequestToJoin() throws HandshakeException {
+
+ try {
+
+
+ while (connectionState == ConnectionStateEnum.WAITING_FOR_HANDSHAKE) {
+
+ AC35Data message = inputQueue.take();
+
+ //We need to wait until they actually send a join request.
+ if (message.getType() == MessageType.REQUEST_TO_JOIN) {
+ return (RequestToJoin) message;
+ }
+
+ }
+
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " was interrupted while waiting on the incoming message queue.", e);
+
+ }
+
+
+ throw new HandshakeException("Handshake was cancelled. Connection state is now: " + connectionState);
+
+ }
+
+
+ /**
+ * Sends the client a {@link JoinAcceptance} message, containing their assigned sourceID.
+ * @param sourceID The sourceID to assign to client.
+ * @throws HandshakeException Thrown if the thread is interrupted while placing message on the outgoing message queue.
+ */
+ private void sendJoinAcceptanceMessage(int sourceID) throws HandshakeException {
+
+ //Send them the source ID.
+ JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, sourceID);
+
+ try {
+ outputQueue.put(joinAcceptance);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new HandshakeException("Handshake failed. Thread: " + Thread.currentThread() + " interrupted while placing JoinAcceptance message on outgoing message queue.", e);
+ }
+
+ }
+
+
+ /**
+ * Determines whether or not this connection is still alive.
+ * This is based off whether the {@link MessageSerialiser} is still alive.
+ * @return True if it is alive, false otherwise.
+ */
+ public boolean isAlive() {
+ return messageSerialiser.isRunning();
+ }
+
+
+ /**
+ * Terminates this connection.
+ */
+ public void terminate() {
+
+ if (this.heartBeatThread != null) {
+ this.heartBeatThread.interrupt();
+ }
+
+ if (this.mockOutputThread != null) {
+ this.mockOutputThread.interrupt();
+ }
+
+ if (this.controllerServerThread != null) {
+ this.controllerServerThread.interrupt();
+ }
+ }
+
+}
diff --git a/racevisionGame/src/main/java/mock/model/ConstantWindGenerator.java b/racevisionGame/src/main/java/mock/model/ConstantWindGenerator.java
new file mode 100644
index 00000000..ae14daac
--- /dev/null
+++ b/racevisionGame/src/main/java/mock/model/ConstantWindGenerator.java
@@ -0,0 +1,54 @@
+package mock.model;
+
+
+import shared.model.Bearing;
+import shared.model.Wind;
+
+import java.util.Random;
+
+/**
+ * This class generates Wind objects for use in a MockRace.
+ * Initialised with a baseline wind speed and direction, and keeps it constant.
+ */
+public class ConstantWindGenerator implements WindGenerator {
+
+ /**
+ * The bearing the wind direction starts at.
+ */
+ private Bearing windBaselineBearing;
+
+
+ /**
+ * The speed the wind starts at, in knots.
+ */
+ private double windBaselineSpeed;
+
+
+
+ /**
+ * Creates a constant wind generator, with a baseline wind speed and direction.
+ * @param windBaselineBearing Baseline wind direction.
+ * @param windBaselineSpeed Baseline wind speed, in knots.
+ */
+ public ConstantWindGenerator(Bearing windBaselineBearing, double windBaselineSpeed) {
+
+ this.windBaselineBearing = windBaselineBearing;
+ this.windBaselineSpeed = windBaselineSpeed;
+
+ }
+
+
+ @Override
+ public Wind generateBaselineWind() {
+ return new Wind(windBaselineBearing, windBaselineSpeed);
+ }
+
+
+ @Override
+ public Wind generateNextWind(Wind currentWind) {
+
+ return generateBaselineWind();
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/mock/model/HeartBeatService.java b/racevisionGame/src/main/java/mock/model/HeartBeatService.java
new file mode 100644
index 00000000..0028a575
--- /dev/null
+++ b/racevisionGame/src/main/java/mock/model/HeartBeatService.java
@@ -0,0 +1,110 @@
+package mock.model;
+
+import network.Messages.AC35Data;
+import network.Messages.HeartBeat;
+import shared.model.RunnableWithFramePeriod;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * This class is responsible for sending {@link HeartBeat} messages to queue.
+ */
+public class HeartBeatService implements RunnableWithFramePeriod {
+
+ /**
+ * Timestamp of the last sent heartbeat message.
+ */
+ private long lastHeartbeatTime;
+
+ /**
+ * Period for the heartbeat - that is, how often we send it. Milliseconds.
+ */
+ private long heartbeatPeriod = 2500;
+
+
+ /**
+ * The messages we're writing to the stream.
+ */
+ private BlockingQueue messagesToSend;
+
+
+
+ /**
+ * Sequence number for heartbeat messages.
+ */
+ private int heartbeatSequenceNum = 1;
+
+
+ /**
+ * Constructs a new HeartBeatService to send heartBeat messages to a given outputStream.
+ * @param messagesToSend The queue to send heartBeat messages to.
+ */
+ public HeartBeatService(BlockingQueue messagesToSend) {
+ this.messagesToSend = messagesToSend;
+ this.lastHeartbeatTime = System.currentTimeMillis();
+ }
+
+
+
+
+ /**
+ * Increments the {@link #heartbeatSequenceNum} value, and returns it.
+ * @return Incremented heat beat number.
+ */
+ private int getNextHeartBeatNumber(){
+ this.heartbeatSequenceNum++;
+
+ return this.heartbeatSequenceNum;
+ }
+
+
+
+ /**
+ * Generates the next heartbeat message and returns it. Increments the heartbeat sequence number.
+ * @return The next heartbeat message.
+ */
+ private HeartBeat createHeartbeatMessage() {
+
+ HeartBeat heartBeat = new HeartBeat(getNextHeartBeatNumber());
+
+ return heartBeat;
+ }
+
+
+ /**
+ * Puts a HeartBeat message on the message queue.
+ * @throws InterruptedException Thrown if the thread is interrupted.
+ */
+ private void sendHeartBeat() throws InterruptedException {
+
+ HeartBeat heartBeat = createHeartbeatMessage();
+
+ messagesToSend.put(heartBeat);
+ }
+
+
+
+ @Override
+ public void run() {
+
+ while (!Thread.interrupted()) {
+ long currentFrameTime = System.currentTimeMillis();
+ waitForFramePeriod(lastHeartbeatTime, currentFrameTime, heartbeatPeriod);
+ lastHeartbeatTime = currentFrameTime;
+
+ try {
+ sendHeartBeat();
+
+ } catch (InterruptedException e) {
+ Logger.getGlobal().log(Level.WARNING, "HeartBeatService: " + this + " sendHeartBeat() was interrupted on thread: " + Thread.currentThread(), e);
+ Thread.currentThread().interrupt();
+ return;
+
+ }
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java
index 63ebf278..7df41b1f 100644
--- a/racevisionGame/src/main/java/mock/model/MockRace.java
+++ b/racevisionGame/src/main/java/mock/model/MockRace.java
@@ -6,6 +6,8 @@ import network.Messages.LatestMessages;
import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import shared.dataInput.RegattaDataSource;
+import shared.exceptions.BoatNotFoundException;
+import shared.enums.RoundingType;
import shared.model.*;
import java.time.ZonedDateTime;
@@ -313,6 +315,8 @@ public class MockRace extends Race {
if (!finish && totalElapsedMilliseconds >= updatePeriodMilliseconds) {
+ checkPosition(boat, totalElapsedMilliseconds);
+
if (boat.getCurrentSpeed() == 0) {
newOptimalVMG(boat);
boat.setBearing(boat.calculateBearingToNextMarker());
@@ -372,6 +376,251 @@ public class MockRace extends Race {
}
}
+ /**
+ * Calculates the upper and lower bounds that the boat may have in order to not go outside of the course.
+ * @param boat The boat to check.
+ * @return An array of bearings. The first is the lower bound, the second is the upper bound.
+ */
+ private Bearing[] calculateBearingBounds(MockBoat boat) {
+
+ Bearing[] bearings = new Bearing[2];
+
+ Bearing lowerBearing = Bearing.fromDegrees(0.001);
+ Bearing upperBearing = Bearing.fromDegrees(359.999);
+
+
+
+ double lastAngle = -1;
+ boolean lastAngleWasGood = false;
+
+ //Check all bearings between [0, 360).
+ for (double angle = 0; angle < 360; angle += 1) {
+
+ //Create bearing from angle.
+ Bearing bearing = Bearing.fromDegrees(angle);
+
+ //Check that if it is acceptable.
+ boolean bearingIsGood = this.checkBearingInsideCourse(bearing, boat.getCurrentPosition());
+
+
+ if (lastAngle != -1) {
+
+ if (lastAngleWasGood && !bearingIsGood) {
+ //We have flipped over from good bearings to bad bearings. So the last good bearing is the upper bearing.
+ upperBearing = Bearing.fromDegrees(lastAngle);
+ }
+
+ if (!lastAngleWasGood && bearingIsGood) {
+ //We have flipped over from bad bearings to good bearings. So the current bearing is the lower bearing.
+ lowerBearing = Bearing.fromDegrees(angle);
+ }
+
+ }
+
+ lastAngle = angle;
+ lastAngleWasGood = bearingIsGood;
+
+ }
+
+
+
+ //TODO BUG if it can't find either upper or lower, it returns (0, 359.999). Should return (boatbearing, boatbearing+0.0001)
+ bearings[0] = lowerBearing;
+ bearings[1] = upperBearing;
+
+ return bearings;
+ }
+
+
+
+ /**
+ * Checks if a given bearing, starting at a given position, would put a boat out of the course boundaries.
+ * @param bearing The bearing to check.
+ * @param position The position to start from.
+ * @return True if the bearing would keep the boat in the course, false if it would take it out of the course.
+ */
+ private boolean checkBearingInsideCourse(Bearing bearing, GPSCoordinate position) {
+
+ //Get azimuth from bearing.
+ Azimuth azimuth = Azimuth.fromBearing(bearing);
+
+
+ //Tests to see if a point in front of the boat is out of bounds.
+ double epsilonMeters = 50d;
+ GPSCoordinate testCoord = GPSCoordinate.calculateNewPosition(position, epsilonMeters, azimuth);
+
+ //If it isn't inside the boundary, calculate new bearing.
+ if (GPSCoordinate.isInsideBoundary(testCoord, this.shrinkBoundary)) {
+ return true;
+ } else {
+ return false;
+ }
+
+ }
+
+ /**
+ * Checks to be run on boats rounding marks on the port side
+ * @param boat the boat that is rounding a mark
+ * @param roundingChecks the checks to run
+ * @param legBearing the direction of the leg
+ */
+ private void boatRoundingCheckPort(MockBoat boat, List roundingChecks, Bearing legBearing) {
+ //boats must pass all checks in order to round a mark
+
+ //boolean for if boat has to/needs to pass through a gate
+ boolean gateCheck = boat.getCurrentLeg().getEndCompoundMark().getMark2() == null || boat.isBetweenGate(boat.getCurrentLeg().getEndCompoundMark());
+ Mark roundingMark = boat.getCurrentLeg().getEndCompoundMark().getMarkForRounding(legBearing);
+
+ switch (boat.getRoundingStatus()) {
+ case 0://hasn't started rounding
+ if (boat.isPortSide(roundingMark) &&
+ GPSCoordinate.passesLine(roundingMark.getPosition(),
+ roundingChecks.get(0), boat.getCurrentPosition(), legBearing) &&
+ gateCheck && boat.isBetweenGate(roundingMark, Mark.tempMark(roundingChecks.get(0)))) {
+ boat.increaseRoundingStatus();
+ if (boat.getCurrentLeg().getLegNumber() + 2 >= legs.size()){
+ //boat has finished race
+ boat.increaseRoundingStatus();
+ }
+ }
+ break;
+ case 1://has been parallel to the mark;
+ if (boat.isPortSide(roundingMark) &&
+ GPSCoordinate.passesLine(roundingMark.getPosition(),
+ roundingChecks.get(1), boat.getCurrentPosition(),
+ Bearing.fromDegrees(legBearing.degrees() - 90)) &&//negative 90 from bearing because of port rounding
+ boat.isBetweenGate(roundingMark, Mark.tempMark(roundingChecks.get(1)))) {
+ boat.increaseRoundingStatus();
+ }
+ break;
+ case 2://has traveled 180 degrees around the mark
+ //Move boat on to next leg.
+ boat.resetRoundingStatus();
+ Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1);
+ boat.setCurrentLeg(nextLeg);
+ break;
+ }
+ }
+
+ /**
+ * Checks to be run on boats rounding marks on the starboard side
+ * @param boat the boat that is rounding a mark
+ * @param roundingChecks the checks to run
+ * @param legBearing the direction of the leg
+ */
+ private void boatRoundingCheckStarboard(MockBoat boat, List roundingChecks, Bearing legBearing){
+ //boats must pass all checks in order to round a mark
+
+ //boolean for if boat has to/needs to pass through a gate
+ boolean gateCheck = boat.getCurrentLeg().getEndCompoundMark().getMark2() == null || boat.isBetweenGate(boat.getCurrentLeg().getEndCompoundMark());
+ Mark roundingMark = boat.getCurrentLeg().getEndCompoundMark().getMarkForRounding(legBearing);
+
+ switch (boat.getRoundingStatus()) {
+ case 0://hasn't started rounding
+ if (boat.isStarboardSide(roundingMark) &&
+ GPSCoordinate.passesLine(roundingMark.getPosition(),
+ roundingChecks.get(0), boat.getCurrentPosition(), legBearing) &&
+ gateCheck &&
+ boat.isBetweenGate(roundingMark, Mark.tempMark(roundingChecks.get(0)))) {
+ boat.increaseRoundingStatus();
+ if (boat.getCurrentLeg().getLegNumber() + 2 >= legs.size()){
+ //boat has finished race
+ boat.increaseRoundingStatus();
+ }
+ }
+ break;
+ case 1://has been parallel to the mark
+ if (boat.isStarboardSide(roundingMark) &&
+ GPSCoordinate.passesLine(roundingMark.getPosition(),
+ roundingChecks.get(1), boat.getCurrentPosition(), Bearing.fromDegrees(legBearing.degrees() + 90)) && //positive 90 from bearing because of starboard rounding
+ boat.isBetweenGate(roundingMark, Mark.tempMark(roundingChecks.get(1)))) {
+ boat.increaseRoundingStatus();
+ }
+ break;
+ case 2://has traveled 180 degrees around the mark
+ //Move boat on to next leg.
+ boat.resetRoundingStatus();
+ Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1);
+ boat.setCurrentLeg(nextLeg);
+ break;
+ }
+ }
+
+ /**
+ * Checks if a boat has finished any legs, or has pulled out of race (DNF).
+ * @param boat The boat to check.
+ * @param timeElapsed The total time, in milliseconds, that has elapsed since the race started.
+ */
+ protected void checkPosition(MockBoat boat, long timeElapsed) {
+ //The distance, in nautical miles, within which the boat needs to get in order to consider that it has reached the marker.
+ double epsilonNauticalMiles = boat.getCurrentLeg().getEndCompoundMark().getRoundingDistance(); //250 meters.
+
+ if (boat.calculateDistanceToNextMarker() < epsilonNauticalMiles) {
+ //Boat is within an acceptable distance from the mark.
+
+ GPSCoordinate startDirectionLinePoint = boat.getCurrentLeg().getStartCompoundMark().getMark1Position();
+ GPSCoordinate endDirectionLinePoint = boat.getCurrentLeg().getEndCompoundMark().getMark1Position();
+ Bearing bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, endDirectionLinePoint);
+
+ //use the direction line to create three invisible points that act as crossover lines a boat must cross
+ //to round a mark
+
+ double bearingToAdd;
+ if (boat.getCurrentLeg().getEndCompoundMark().getRoundingType() == RoundingType.Port ||
+ boat.getCurrentLeg().getEndCompoundMark().getRoundingType() == RoundingType.SP){
+ bearingToAdd = 90;
+ }else{
+ bearingToAdd = -90;
+ }
+
+ GPSCoordinate roundCheck1 = GPSCoordinate.calculateNewPosition(endDirectionLinePoint,
+ epsilonNauticalMiles, Azimuth.fromDegrees(bearingOfDirectionLine.degrees() + bearingToAdd));
+
+ GPSCoordinate roundCheck2;
+ try{
+ Leg nextLeg = legs.get(legs.indexOf(boat.getCurrentLeg()) + 1);
+
+ GPSCoordinate startNextDirectionLinePoint = nextLeg.getStartCompoundMark().getMark1Position();
+ GPSCoordinate endNextDirectionLinePoint = nextLeg.getEndCompoundMark().getMark1Position();
+ Bearing bearingOfNextDirectionLine = GPSCoordinate.calculateBearing(startNextDirectionLinePoint, endNextDirectionLinePoint);
+
+ roundCheck2 = GPSCoordinate.calculateNewPosition(endDirectionLinePoint,
+ epsilonNauticalMiles, Azimuth.fromDegrees(bearingOfNextDirectionLine.degrees() + bearingToAdd));
+ }catch(NullPointerException e){
+ //this is caused by the last leg not being having a leg after it
+ roundCheck2 = roundCheck1;
+ }
+
+ List roundingChecks = new ArrayList(Arrays.asList(roundCheck1, roundCheck2));
+
+ switch (boat.getCurrentLeg().getEndCompoundMark().getRoundingType()) {
+ case SP://Not yet implemented so these gates will be rounded port side
+ case Port:
+ boatRoundingCheckPort(boat, roundingChecks, bearingOfDirectionLine);
+ break;
+ case PS://not yet implemented so these gates will be rounded starboard side
+ case Starboard:
+ boatRoundingCheckStarboard(boat, roundingChecks, bearingOfDirectionLine);
+ break;
+ }
+
+
+ //Check if the boat has finished or stopped racing.
+ if (this.isLastLeg(boat.getCurrentLeg())) {
+ //Boat has finished.
+ boat.setTimeFinished(timeElapsed);
+ boat.setCurrentSpeed(0);
+ boat.setStatus(BoatStatusEnum.FINISHED);
+
+ }
+
+ }
+
+ }
+
+
+
+
/**
* Returns the number of boats that are still active in the race.
* They become inactive by either finishing or withdrawing.
@@ -402,6 +651,25 @@ public class MockRace extends Race {
return boats;
}
+ /**
+ * Returns a boat by sourceID.
+ * @param sourceID The source ID the boat.
+ * @return The boat.
+ * @throws BoatNotFoundException Thrown if there is not boat with the specified sourceID.
+ */
+ public MockBoat getBoat(int sourceID) throws BoatNotFoundException {
+
+ for (MockBoat boat : boats) {
+
+ if (boat.getSourceID() == sourceID) {
+ return boat;
+ }
+
+ }
+
+ throw new BoatNotFoundException("Boat with sourceID: " + sourceID + " was not found.");
+ }
+
/**
* Changes the wind direction randomly, while keeping it within [windLowerBound, windUpperBound].
*/
@@ -434,7 +702,6 @@ public class MockRace extends Race {
}
- public List getCompoundMarks() {
- return compoundMarks;
- }
-}
\ No newline at end of file
+
+
+}
diff --git a/racevisionGame/src/main/java/mock/model/RaceLogic.java b/racevisionGame/src/main/java/mock/model/RaceLogic.java
index 42265646..418ade17 100644
--- a/racevisionGame/src/main/java/mock/model/RaceLogic.java
+++ b/racevisionGame/src/main/java/mock/model/RaceLogic.java
@@ -1,18 +1,16 @@
package mock.model;
import javafx.animation.AnimationTimer;
-import mock.model.commandFactory.CommandFactory;
import mock.model.commandFactory.CompositeCommand;
+import mock.model.commandFactory.CommandFactory;
import network.Messages.Enums.BoatActionEnum;
import network.Messages.Enums.BoatStatusEnum;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.LatestMessages;
-import visualiser.gameController.ControllerServer;
+import shared.model.RunnableWithFramePeriod;
-import java.util.Observable;
-import java.util.Observer;
-public class RaceLogic implements Observer, Runnable {
+public class RaceLogic implements RunnableWithFramePeriod {
/**
* State of current race modified by this object
*/
@@ -28,11 +26,12 @@ public class RaceLogic implements Observer, Runnable {
* Initialises race loop with state and server message queue
* @param race state of race to modify
* @param messages to send to server
+ * @param compositeCommand Commands from clients to execute.
*/
- public RaceLogic(MockRace race, LatestMessages messages) {
+ public RaceLogic(MockRace race, LatestMessages messages, CompositeCommand compositeCommand) {
this.race = race;
this.server = new RaceServer(race, messages);
- this.commands = new CompositeCommand();
+ this.commands = compositeCommand;
}
/**
@@ -41,20 +40,23 @@ public class RaceLogic implements Observer, Runnable {
@Override
public void run() {
race.initialiseBoats();
- this.countdownTimer.start();
+
+ countdown();
+
+ raceLoop();
}
/**
* Countdown timer until race starts.
*/
- protected AnimationTimer countdownTimer = new AnimationTimer() {
+ private void countdown() {
+ long previousFrameTime = System.currentTimeMillis();
- long currentTime = System.currentTimeMillis();
+ while (race.getRaceStatusEnum() != RaceStatusEnum.STARTED) {
- @Override
- public void handle(long arg0) {
+ long currentTime = System.currentTimeMillis();
//Update race time.
race.updateRaceTime(currentTime);
@@ -65,58 +67,39 @@ public class RaceLogic implements Observer, Runnable {
//Provide boat's with an estimated time at next mark until the race starts.
race.setBoatsTimeNextMark(race.getRaceClock().getCurrentTime());
- //Parse the boat locations.
- server.parseBoatLocations();
+ //Parse the race snapshot.
+ server.parseSnapshot();
- //Parse the marks.
- server.parseMarks();
// Change wind direction
race.changeWindDirection();
- //Parse the race status.
- server.parseRaceStatus();
-
if (race.getRaceStatusEnum() == RaceStatusEnum.STARTED) {
race.setBoatsStatusToRacing();
- raceTimer.start();
- this.stop();
}
- //Update the animations timer's time.
- currentTime = System.currentTimeMillis();
+ waitForFramePeriod(previousFrameTime, currentTime, 50);
+ previousFrameTime = currentTime;
+
}
- };
+ }
/**
* Timer that runs for the duration of the race, until all boats finish.
*/
- private AnimationTimer raceTimer = new AnimationTimer() {
-
- /**
- * Start time of loop, in milliseconds.
- */
- long timeRaceStarted = System.currentTimeMillis();
-
- /**
- * Current time during a loop iteration.
- */
- long currentTime = System.currentTimeMillis();
-
- /**
- * The time of the previous frame, in milliseconds.
- */
- long lastFrameTime = timeRaceStarted;
+ private void raceLoop() {
- long framePeriod = currentTime - lastFrameTime;
+ long previousFrameTime = System.currentTimeMillis();
- @Override
- public void handle(long arg0) {
+ while (race.getRaceStatusEnum() != RaceStatusEnum.FINISHED) {
//Get the current time.
- currentTime = System.currentTimeMillis();
+ long currentTime = System.currentTimeMillis();
+
+ //Execute commands from clients.
+ commands.execute();
//Update race time.
race.updateRaceTime(currentTime);
@@ -125,16 +108,15 @@ public class RaceLogic implements Observer, Runnable {
if (race.getNumberOfActiveBoats() != 0) {
//Get the time period of this frame.
- framePeriod = currentTime - lastFrameTime;
+ long framePeriod = currentTime - previousFrameTime;
//For each boat, we update its position, and generate a BoatLocationMessage.
for (MockBoat boat : race.getBoats()) {
//If it is still racing, update its position.
if (boat.getStatus() == BoatStatusEnum.RACING) {
- commands.execute();
race.updatePosition(boat, framePeriod, race.getRaceClock().getDurationMilli());
- race.getColliderRegistry().rayCast(boat);
+
}
}
@@ -143,28 +125,23 @@ public class RaceLogic implements Observer, Runnable {
//Otherwise, the race is over!
raceFinished.start();
race.setRaceStatusEnum(RaceStatusEnum.FINISHED);
- this.stop();
}
if (race.getNumberOfActiveBoats() != 0) {
// Change wind direction
race.changeWindDirection();
- //Parse the boat locations.
- server.parseBoatLocations();
-
- //Parse the marks.
- server.parseMarks();
-
- //Parse the race status.
- server.parseRaceStatus();
-
+ //Parse the race snapshot.
+ server.parseSnapshot();
//Update the last frame time.
- this.lastFrameTime = currentTime;
+ previousFrameTime = currentTime;
}
+
+ waitForFramePeriod(previousFrameTime, currentTime, 50);
+ previousFrameTime = currentTime;
}
- };
+ }
/**
* Broadcast that the race has finished.
@@ -174,7 +151,7 @@ public class RaceLogic implements Observer, Runnable {
@Override
public void handle(long now) {
- server.parseRaceStatus();
+ server.parseSnapshot();
if (iters > 500) {
stop();
@@ -183,12 +160,12 @@ public class RaceLogic implements Observer, Runnable {
}
};
- @Override
- public void update(Observable o, Object arg) {
- ControllerServer server = (ControllerServer)o;
- BoatActionEnum action = server.getAction();
- MockBoat boat = race.getBoats().get(0);
- commands.addCommand(CommandFactory.createCommand(race, boat, action));
+ /**
+ * Returns the race state that this RaceLogic is simulating.
+ * @return Race state this RaceLogic is simulating.
+ */
+ public MockRace getRace() {
+ return race;
}
}
diff --git a/racevisionGame/src/main/java/mock/model/RaceServer.java b/racevisionGame/src/main/java/mock/model/RaceServer.java
index c7e3ab69..969a4c71 100644
--- a/racevisionGame/src/main/java/mock/model/RaceServer.java
+++ b/racevisionGame/src/main/java/mock/model/RaceServer.java
@@ -1,10 +1,9 @@
package mock.model;
-import network.Messages.BoatLocation;
-import network.Messages.BoatStatus;
-import network.Messages.LatestMessages;
-import network.Messages.RaceStatus;
+import network.Messages.*;
+import network.Messages.Enums.BoatLocationDeviceEnum;
import network.Utils.AC35UnitConverter;
+import shared.model.Bearing;
import shared.model.CompoundMark;
import shared.model.Constants;
import shared.model.Mark;
@@ -19,10 +18,6 @@ public class RaceServer {
private MockRace race;
private LatestMessages latestMessages;
- /**
- * The sequence number of the latest RaceStatus message sent or received.
- */
- private int raceStatusSequenceNumber = 1;
/**
* The sequence number of the latest BoatLocation message sent or received.
@@ -37,29 +32,56 @@ public class RaceServer {
/**
- * Parses an individual marker boat, and sends it to mockOutput.
+ * Parses the race to create a snapshot, and places it in latestMessages.
+ */
+ public void parseSnapshot() {
+
+ List snapshotMessages = new ArrayList<>();
+
+ //Parse the boat locations.
+ snapshotMessages.addAll(parseBoatLocations());
+
+ //Parse the marks.
+ snapshotMessages.addAll(parseMarks());
+
+ //Parse the race status.
+ snapshotMessages.add(parseRaceStatus());
+
+ latestMessages.setSnapshot(snapshotMessages);
+ }
+
+
+ /**
+ * Parses an individual marker boat, and returns it.
* @param mark The marker boat to parse.
+ * @return The BoatLocation message.
*/
- private void parseIndividualMark(Mark mark) {
+ private BoatLocation parseIndividualMark(Mark mark) {
//Create message.
BoatLocation boatLocation = new BoatLocation(
mark.getSourceID(),
mark.getPosition().getLatitude(),
mark.getPosition().getLongitude(),
this.boatLocationSequenceNumber,
- 0, 0,
+ BoatLocationDeviceEnum.Mark,
+ Bearing.fromDegrees(0),
+ 0,
race.getRaceClock().getCurrentTimeMilli());
//Iterates the sequence number.
this.boatLocationSequenceNumber++;
- this.latestMessages.setBoatLocation(boatLocation);
+ return boatLocation;
}
/**
- * Parse the compound marker boats through mock output.
+ * Parse the compound marker boats, and returns a list of BoatLocation messages.
+ * @return BoatLocation messages for each mark.
*/
- public void parseMarks() {
+ private List parseMarks() {
+
+ List markLocations = new ArrayList<>(race.getCompoundMarks().size());
+
for (CompoundMark compoundMark : race.getCompoundMarks()) {
//Get the individual marks from the compound mark.
@@ -68,54 +90,65 @@ public class RaceServer {
//If they aren't null, parse them (some compound marks only have one mark).
if (mark1 != null) {
- this.parseIndividualMark(mark1);
+ markLocations.add(this.parseIndividualMark(mark1));
}
if (mark2 != null) {
- this.parseIndividualMark(mark2);
+ markLocations.add(this.parseIndividualMark(mark2));
}
}
+
+ return markLocations;
}
/**
- * Parse the boats in the race, and send it to mockOutput.
+ * Parse the boats in the race, and returns all of their BoatLocation messages.
+ * @return List of BoatLocation messages, for each boat.
*/
- public void parseBoatLocations() {
+ private List parseBoatLocations() {
+
+ List boatLocations = new ArrayList<>(race.getBoats().size());
+
//Parse each boat.
for (MockBoat boat : race.getBoats()) {
- this.parseIndividualBoatLocation(boat);
+ boatLocations.add(this.parseIndividualBoatLocation(boat));
}
+
+ return boatLocations;
}
/**
- * Parses an individual boat, and sends it to mockOutput.
+ * Parses an individual boat, and returns it.
* @param boat The boat to parse.
+ * @return The BoatLocation message.
*/
- private void parseIndividualBoatLocation(MockBoat boat) {
+ private BoatLocation parseIndividualBoatLocation(MockBoat boat) {
BoatLocation boatLocation = new BoatLocation(
boat.getSourceID(),
boat.getCurrentPosition().getLatitude(),
boat.getCurrentPosition().getLongitude(),
this.boatLocationSequenceNumber,
- boat.getBearing().degrees(),
+ BoatLocationDeviceEnum.RacingYacht,
+ boat.getBearing(),
boat.getCurrentSpeed(),
race.getRaceClock().getCurrentTimeMilli());
//Iterates the sequence number.
this.boatLocationSequenceNumber++;
- this.latestMessages.setBoatLocation(boatLocation);
+ return boatLocation;
}
/**
- * Parses the race status, and sends it to mockOutput.
+ * Parses the race status, and returns it.
+ * @return The race status message.
*/
- public void parseRaceStatus() {
+ private RaceStatus parseRaceStatus() {
//A race status message contains a list of boat statuses.
List boatStatuses = new ArrayList<>();
@@ -133,21 +166,19 @@ public class RaceServer {
}
- //Convert wind direction and speed to ints. //TODO this conversion should be done inside the racestatus class.
- int windDirectionInt = AC35UnitConverter.encodeHeading(race.getWindDirection().degrees());
- int windSpeedInt = (int) (race.getWindSpeed() * Constants.KnotsToMMPerSecond);
//Create race status object, and send it.
RaceStatus raceStatus = new RaceStatus(
+ RaceStatus.currentMessageVersionNumber,
System.currentTimeMillis(),
race.getRaceId(),
- race.getRaceStatusEnum().getValue(),
+ race.getRaceStatusEnum(),
race.getRaceClock().getStartingTimeMilli(),
- windDirectionInt,
- windSpeedInt,
- race.getRaceType().getValue(),
+ race.getWindDirection(),
+ race.getWindSpeed(),
+ race.getRaceType(),
boatStatuses);
- this.latestMessages.setRaceStatus(raceStatus);
+ return raceStatus;
}
}
diff --git a/racevisionGame/src/main/java/mock/model/RandomWindGenerator.java b/racevisionGame/src/main/java/mock/model/RandomWindGenerator.java
new file mode 100644
index 00000000..4f981b8d
--- /dev/null
+++ b/racevisionGame/src/main/java/mock/model/RandomWindGenerator.java
@@ -0,0 +1,242 @@
+package mock.model;
+
+
+import shared.model.Bearing;
+import shared.model.Wind;
+
+import java.util.Random;
+
+/**
+ * This class generates Wind objects for use in a MockRace.
+ * Bounds on bearing and speed can be specified.
+ * Wind can be completely random, or random incremental change.
+ */
+public class RandomWindGenerator implements WindGenerator {
+
+ /**
+ * The bearing the wind direction starts at.
+ */
+ private Bearing windBaselineBearing;
+
+ /**
+ * The lower bearing angle that the wind may have.
+ */
+ private Bearing windBearingLowerBound;
+
+ /**
+ * The upper bearing angle that the wind may have.
+ */
+ private Bearing windBearingUpperBound;
+
+
+
+ /**
+ * The speed the wind starts at, in knots.
+ */
+ private double windBaselineSpeed;
+
+ /**
+ * The lower speed that the wind may have, in knots.
+ */
+ private double windSpeedLowerBound;
+
+ /**
+ * The upper speed that the wind may have, in knots.
+ */
+ private double windSpeedUpperBound;
+
+
+ /**
+ * Creates a wind generator, with a baseline, lower bound, and upper bound, for the wind speed and direction.
+ * @param windBaselineBearing Baseline wind direction.
+ * @param windBearingLowerBound Lower bound for wind direction.
+ * @param windBearingUpperBound Upper bound for wind direction.
+ * @param windBaselineSpeed Baseline wind speed, in knots.
+ * @param windSpeedLowerBound Lower bound for wind speed, in knots.
+ * @param windSpeedUpperBound Upper bound for wind speed, in knots.
+ */
+ public RandomWindGenerator(Bearing windBaselineBearing, Bearing windBearingLowerBound, Bearing windBearingUpperBound, double windBaselineSpeed, double windSpeedLowerBound, double windSpeedUpperBound) {
+
+ this.windBaselineBearing = windBaselineBearing;
+ this.windBearingLowerBound = windBearingLowerBound;
+ this.windBearingUpperBound = windBearingUpperBound;
+ this.windBaselineSpeed = windBaselineSpeed;
+ this.windSpeedLowerBound = windSpeedLowerBound;
+ this.windSpeedUpperBound = windSpeedUpperBound;
+
+ }
+
+
+ @Override
+ public Wind generateBaselineWind() {
+ return new Wind(windBaselineBearing, windBaselineSpeed);
+ }
+
+ /**
+ * Generates a random Wind object, that is within the provided bounds.
+ * @return Generated wind object.
+ */
+ public Wind generateRandomWind() {
+
+ double windSpeed = generateRandomWindSpeed();
+ Bearing windBearing = generateRandomWindBearing();
+
+ return new Wind(windBearing, windSpeed);
+
+ }
+
+ /**
+ * Generates a random wind speed within the specified bounds. In knots.
+ * @return Wind speed, in knots.
+ */
+ private double generateRandomWindSpeed() {
+
+ double randomSpeedKnots = generateRandomValueInBounds(windSpeedLowerBound, windSpeedUpperBound);
+
+ return randomSpeedKnots;
+ }
+
+
+ /**
+ * Generates a random wind bearing within the specified bounds.
+ * @return Wind bearing.
+ */
+ private Bearing generateRandomWindBearing() {
+
+ double randomBearingDegrees = generateRandomValueInBounds(windBearingLowerBound.degrees(), windBearingUpperBound.degrees());
+
+ return Bearing.fromDegrees(randomBearingDegrees);
+ }
+
+
+ /**
+ * Generates a random value within a specified interval.
+ * @param lowerBound The lower bound of the interval.
+ * @param upperBound The upper bound of the interval.
+ * @return A random value within the interval.
+ */
+ private static double generateRandomValueInBounds(double lowerBound, double upperBound) {
+
+ float proportion = new Random().nextFloat();
+
+ double delta = upperBound - lowerBound;
+
+ double amount = delta * proportion;
+
+ double finalAmount = amount + lowerBound;
+
+ return finalAmount;
+
+ }
+
+
+ /**
+ * Generates a new value within an interval, given a start value, chance to change, and change amount.
+ * @param lowerBound Lower bound of interval.
+ * @param upperBound Upper bound of interval.
+ * @param currentValue The current value to change.
+ * @param changeAmount The amount to change by.
+ * @param chanceToChange The change to actually change the value.
+ * @return The new value.
+ */
+ private static double generateNextValueInBounds(double lowerBound, double upperBound, double currentValue, double changeAmount, double chanceToChange) {
+
+ float chance = new Random().nextFloat();
+
+
+ if (chance <= chanceToChange) {
+ currentValue += changeAmount;
+
+ } else if (chance <= (2 * chanceToChange)) {
+ currentValue -= changeAmount;
+
+ }
+
+ currentValue = clamp(lowerBound, upperBound, currentValue);
+
+ return currentValue;
+
+ }
+
+
+ @Override
+ public Wind generateNextWind(Wind currentWind) {
+
+ double windSpeed = generateNextWindSpeed(currentWind.getWindSpeed());
+ Bearing windBearing = generateNextWindBearing(currentWind.getWindDirection());
+
+ return new Wind(windBearing, windSpeed);
+
+ }
+
+
+ /**
+ * Generates the next wind speed to use.
+ * @param windSpeed Current wind speed, in knots.
+ * @return Next wind speed, in knots.
+ */
+ private double generateNextWindSpeed(double windSpeed) {
+
+ double chanceToChange = 0.2;
+ double changeAmount = 0.1;
+
+ double nextWindSpeed = generateNextValueInBounds(
+ windSpeedLowerBound,
+ windSpeedUpperBound,
+ windSpeed,
+ changeAmount,
+ chanceToChange);
+
+ return nextWindSpeed;
+ }
+
+
+ /**
+ * Generates the next wind speed to use.
+ * @param windBearing Current wind bearing.
+ * @return Next wind speed.
+ */
+ private Bearing generateNextWindBearing(Bearing windBearing) {
+
+ double chanceToChange = 0.2;
+ double changeAmount = 0.5;
+
+ double nextWindBearingDegrees = generateNextValueInBounds(
+ windBearingLowerBound.degrees(),
+ windBearingUpperBound.degrees(),
+ windBearing.degrees(),
+ changeAmount,
+ chanceToChange);
+
+ return Bearing.fromDegrees(nextWindBearingDegrees);
+ }
+
+
+
+
+
+ /**
+ * Clamps a value to be within an interval.
+ * @param lower Lower bound of the interval.
+ * @param upper Upper bound of the interval.
+ * @param value Value to clamp.
+ * @return The clamped value.
+ */
+ private static double clamp(double lower, double upper, double value) {
+
+ if (value > upper) {
+ value = upper;
+
+ } else if (value < lower) {
+ value = lower;
+
+ }
+
+ return value;
+ }
+
+
+
+
+
+}
diff --git a/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java b/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java
new file mode 100644
index 00000000..3b62a8a7
--- /dev/null
+++ b/racevisionGame/src/main/java/mock/model/SourceIdAllocator.java
@@ -0,0 +1,70 @@
+package mock.model;
+
+
+import mock.exceptions.SourceIDAllocationException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This class is responsible for allocating boat source IDs for use in a race, upon request.
+ */
+public class SourceIdAllocator {
+
+
+ /**
+ * This list contains all unallocated source IDs.
+ */
+ List unallocatedIDs = new ArrayList<>();
+
+
+ /**
+ * This list contains all allocated source IDs.
+ */
+ List allocatedIDs = new ArrayList<>();
+
+
+ /**
+ * Creates a source ID allocator, using the given list of unallocated source IDs.
+ * @param unallocatedIDs List of unallocated source IDs.
+ */
+ public SourceIdAllocator(List unallocatedIDs) {
+ //We need to copy the list.
+ this.unallocatedIDs.addAll(unallocatedIDs);
+ }
+
+
+ /**
+ * Allocates a source ID for a boat.
+ * @return The allocated source ID.
+ * @throws SourceIDAllocationException Thrown if we cannot allocate any more source IDs.
+ */
+ public synchronized int allocateSourceID() throws SourceIDAllocationException {
+
+ if (!unallocatedIDs.isEmpty()) {
+
+ int sourceID = unallocatedIDs.remove(0);
+
+ allocatedIDs.add(sourceID);
+
+ return sourceID;
+
+ } else {
+ throw new SourceIDAllocationException("Could not allocate a source ID.");
+
+ }
+ }
+
+
+ /**
+ * Returns a source ID to the source ID allocator, so that it can be reused.
+ * @param sourceID Source ID to return.
+ */
+ public void returnSourceID(Integer sourceID) {
+
+ //We remove an Integer, not an int, so that we remove by value not by index.
+ allocatedIDs.remove(sourceID);
+
+ unallocatedIDs.add(sourceID);
+ }
+}
diff --git a/racevisionGame/src/main/java/mock/model/SplitTODO.java b/racevisionGame/src/main/java/mock/model/SplitTODO.java
new file mode 100644
index 00000000..20f14973
--- /dev/null
+++ b/racevisionGame/src/main/java/mock/model/SplitTODO.java
@@ -0,0 +1,555 @@
+//package mock.model;
+//
+//import javafx.animation.AnimationTimer;
+//import network.Messages.BoatLocation;
+//import network.Messages.BoatStatus;
+//import network.Messages.Enums.BoatStatusEnum;
+//import network.Messages.Enums.RaceStatusEnum;
+//import network.Messages.LatestMessages;
+//import network.Messages.RaceStatus;
+//import network.Utils.AC35UnitConverter;
+//import shared.dataInput.BoatDataSource;
+//import shared.dataInput.RaceDataSource;
+//import shared.dataInput.RegattaDataSource;
+//import shared.model.*;
+//
+//import java.time.ZonedDateTime;
+//import java.time.temporal.ChronoUnit;
+//import java.util.ArrayList;
+//import java.util.Iterator;
+//import java.util.List;
+//import java.util.Map;
+//
+//import static java.lang.Math.cos;
+//
+///**
+// * Unused class, copy of MockRace so methods can be deleted once they are moved (more of a checklist)
+// */
+//public class SplitTODO {
+//
+//
+// /**
+// * Represents a yacht race.
+// * Has a course, boats, boundaries, etc...
+// * Is responsible for simulating the race, and sending messages to a MockOutput instance.
+// */
+// public class MockRace extends Race {
+//
+// /**
+// * Constructs a race object with a given RaceDataSource, BoatDataSource, and RegattaDataSource and sends events to the given mockOutput.
+// * @param boatDataSource Data source for boat related data (yachts and marker boats).
+// * @param raceDataSource Data source for race related data (participating boats, legs, etc...).
+// * @param regattaDataSource Data source for race related data (course name, location, timezone, etc...).
+// * @param latestMessages The LatestMessages to send events to.
+// * @param polars The polars table to be used for boat simulation.
+// * @param timeScale The timeScale for the race. See {@link Constants#RaceTimeScale}.
+// */
+// public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale) {
+//
+// super(boatDataSource, raceDataSource, regattaDataSource, latestMessages);
+//
+// this.scaleFactor = timeScale;
+// }
+//
+//
+//
+// /**
+// * Parse the compound marker boats through mock output.
+// */
+// private void parseMarks() {
+// for (CompoundMark compoundMark : this.compoundMarks) {
+//
+// //Get the individual marks from the compound mark.
+// Mark mark1 = compoundMark.getMark1();
+// Mark mark2 = compoundMark.getMark2();
+//
+// //If they aren't null, parse them (some compound marks only have one mark).
+// if (mark1 != null) {
+// this.parseIndividualMark(mark1);
+// }
+//
+// if (mark2 != null) {
+// this.parseIndividualMark(mark2);
+// }
+//
+// }
+// }
+//
+// /**
+// * Parses an individual marker boat, and sends it to mockOutput.
+// * @param mark The marker boat to parse.
+// */
+// private void parseIndividualMark(Mark mark) {
+//
+// //Create message.
+// BoatLocation boatLocation = new BoatLocation(
+// mark.getSourceID(),
+// mark.getPosition().getLatitude(),
+// mark.getPosition().getLongitude(),
+// this.boatLocationSequenceNumber,
+// 0, 0,
+// this.raceClock.getCurrentTimeMilli());
+//
+// //Iterates the sequence number.
+// this.boatLocationSequenceNumber++;
+//
+// this.latestMessages.setBoatLocation(boatLocation);
+//
+//
+// }
+//
+// /**
+// * Parse the boats in the race, and send it to mockOutput.
+// */
+// private void parseBoatLocations() {
+//
+// //Parse each boat.
+// for (MockBoat boat : this.boats) {
+//
+// this.parseIndividualBoatLocation(boat);
+//
+// }
+//
+// }
+//
+// /**
+// * Parses an individual boat, and sends it to mockOutput.
+// * @param boat The boat to parse.
+// */
+// private void parseIndividualBoatLocation(MockBoat boat) {
+//
+// BoatLocation boatLocation = new BoatLocation(
+// boat.getSourceID(),
+// boat.getCurrentPosition().getLatitude(),
+// boat.getCurrentPosition().getLongitude(),
+// this.boatLocationSequenceNumber,
+// boat.getBearing().degrees(),
+// boat.getCurrentSpeed(),
+// this.raceClock.getCurrentTimeMilli());
+//
+// //Iterates the sequence number.
+// this.boatLocationSequenceNumber++;
+//
+// this.latestMessages.setBoatLocation(boatLocation);
+//
+// }
+//
+//
+// /**
+// * Updates the race time to a specified value, in milliseconds since the unix epoch.
+// * @param currentTime Milliseconds since unix epoch.
+// */
+// private void updateRaceTime(long currentTime) {
+// this.raceClock.setUTCTime(currentTime);
+// }
+//
+//
+// /**
+// * Updates the race status enumeration based on the current time.
+// */
+// private void updateRaceStatusEnum() {
+//
+// //The millisecond duration of the race. Negative means it hasn't started, so we flip sign.
+// long timeToStart = - this.raceClock.getDurationMilli();
+//
+//
+// if (timeToStart > Constants.RacePreStartTime) {
+// //Time > 3 minutes is the prestart period.
+// this.setRaceStatusEnum(RaceStatusEnum.PRESTART);
+//
+// } else if ((timeToStart <= Constants.RacePreStartTime) && (timeToStart >= Constants.RacePreparatoryTime)) {
+// //Time between [1, 3] minutes is the warning period.
+// this.setRaceStatusEnum(RaceStatusEnum.WARNING);
+//
+// } else if ((timeToStart <= Constants.RacePreparatoryTime) && (timeToStart > 0)) {
+// //Time between (0, 1] minutes is the preparatory period.
+// this.setRaceStatusEnum(RaceStatusEnum.PREPARATORY);
+//
+// } else {
+// //Otherwise, the race has started!
+// this.setRaceStatusEnum(RaceStatusEnum.STARTED);
+//
+// }
+//
+//
+// }
+//
+// /**
+// * Parses the race status, and sends it to mockOutput.
+// */
+// private void parseRaceStatus() {
+//
+// //A race status message contains a list of boat statuses.
+// List boatStatuses = new ArrayList<>();
+//
+// //Add each boat status to the status list.
+// for (MockBoat boat : this.boats) {
+//
+// BoatStatus boatStatus = new BoatStatus(
+// boat.getSourceID(),
+// boat.getStatus(),
+// boat.getCurrentLeg().getLegNumber(),
+// boat.getEstimatedTimeAtNextMark().toInstant().toEpochMilli() );
+//
+// boatStatuses.add(boatStatus);
+// }
+//
+//
+// //Convert wind direction and speed to ints. //TODO this conversion should be done inside the racestatus class.
+// int windDirectionInt = AC35UnitConverter.encodeHeading(this.getWindDirection().degrees());
+// int windSpeedInt = (int) (this.getWindSpeed() * Constants.KnotsToMMPerSecond);
+//
+// //Create race status object, and send it.
+// RaceStatus raceStatus = new RaceStatus(
+// System.currentTimeMillis(),
+// this.raceId,
+// this.getRaceStatusEnum().getValue(),
+// this.raceClock.getStartingTimeMilli(),
+// windDirectionInt,
+// windSpeedInt,
+// this.getRaceType().getValue(),
+// boatStatuses);
+//
+//
+// this.latestMessages.setRaceStatus(raceStatus);
+//
+//
+// }
+//
+//
+// /**
+// * Sets the status of all boats in the race to RACING.
+// */
+// private void setBoatsStatusToRacing() {
+//
+// for (MockBoat boat : this.boats) {
+// boat.setStatus(BoatStatusEnum.RACING);
+// }
+// }
+//
+//
+// /**
+// * Sets the estimated time at next mark for each boat to a specified time. This is used during the countdown timer to provide this value to boat before the race starts.
+// * @param time The time to provide to each boat.
+// */
+// private void setBoatsTimeNextMark(ZonedDateTime time) {
+//
+// for (MockBoat boat : this.boats) {
+// boat.setEstimatedTimeAtNextMark(time);
+// }
+// }
+//
+//
+// /**
+// * Countdown timer until race starts.
+// */
+// protected AnimationTimer countdownTimer = new AnimationTimer() {
+//
+//
+// long currentTime = System.currentTimeMillis();
+//
+// @Override
+// public void handle(long arg0) {
+//
+// //Update race time.
+// updateRaceTime(currentTime);
+//
+// //Update the race status based on the current time.
+// updateRaceStatusEnum();
+//
+// //Provide boat's with an estimated time at next mark until the race starts.
+// setBoatsTimeNextMark(raceClock.getCurrentTime());
+//
+// //Parse the boat locations.
+// parseBoatLocations();
+//
+// //Parse the marks.
+// parseMarks();
+//
+// // Change wind direction
+// changeWindDirection();
+//
+// //Parse the race status.
+// parseRaceStatus();
+//
+//
+// if (getRaceStatusEnum() == RaceStatusEnum.STARTED) {
+// setBoatsStatusToRacing();
+// raceTimer.start();
+// this.stop();
+// }
+//
+// //Update the animations timer's time.
+// currentTime = System.currentTimeMillis();
+// }
+// };
+//
+//
+// /**
+// * Timer that runs for the duration of the race, until all boats finish.
+// */
+// private AnimationTimer raceTimer = new AnimationTimer() {
+//
+// /**
+// * Start time of loop, in milliseconds.
+// */
+// long timeRaceStarted = System.currentTimeMillis();
+//
+// /**
+// * Current time during a loop iteration.
+// */
+// long currentTime = System.currentTimeMillis();
+//
+// /**
+// * The time of the previous frame, in milliseconds.
+// */
+// long lastFrameTime = timeRaceStarted;
+//
+// @Override
+// public void handle(long arg0) {
+//
+// //Get the current time.
+// currentTime = System.currentTimeMillis();
+//
+// //Update race time.
+// updateRaceTime(currentTime);
+//
+//
+// //As long as there is at least one boat racing, we still simulate the race.
+// if (getNumberOfActiveBoats() != 0) {
+//
+// //Get the time period of this frame.
+// long framePeriod = currentTime - lastFrameTime;
+//
+// //For each boat, we update its position, and generate a BoatLocationMessage.
+// for (MockBoat boat : boats) {
+//
+// //If it is still racing, update its position.
+// if (boat.getStatus() == BoatStatusEnum.RACING) {
+//
+// updatePosition(boat, framePeriod, raceClock.getDurationMilli());
+//
+// }
+//
+// }
+//
+// } else {
+// //Otherwise, the race is over!
+// raceFinished.start();
+// setRaceStatusEnum(RaceStatusEnum.FINISHED);
+// this.stop();
+// }
+//
+// if (getNumberOfActiveBoats() != 0) {
+// // Change wind direction
+// changeWindDirection();
+//
+// //Parse the boat locations.
+// parseBoatLocations();
+//
+// //Parse the marks.
+// parseMarks();
+//
+// //Parse the race status.
+// parseRaceStatus();
+//
+//
+// //Update the last frame time.
+// this.lastFrameTime = currentTime;
+// }
+// }
+// };
+//
+// /**
+// * Broadcast that the race has finished.
+// */
+// protected AnimationTimer raceFinished = new AnimationTimer(){
+// int iters = 0;
+// @Override
+// public void handle(long now) {
+//
+// parseRaceStatus();
+//
+// if (iters > 500) {
+// stop();
+// }
+// iters++;
+// }
+// };
+//
+//
+// /**
+// * Calculates a boat's VMG.
+// * @param boat The boat to calculate VMG for.
+// * @return VMG for the specified boat.
+// */
+// private VMG calculateVMG(MockBoat boat) {
+//
+//
+// //Find the VMG inside these bounds.
+// VMG bestVMG = boat.getPolars().calculateVMG(this.getWindDirection(), this.getWindSpeed(), boat.calculateBearingToNextMarker(), Bearing.fromDegrees(0d), Bearing.fromDegrees(359.99999d));
+//
+//
+// return bestVMG;
+//
+// }
+//
+//
+// /**
+// * Determines whether or not a given VMG improves the velocity of a boat, if it were currently using currentVMG.
+// * @param currentVMG The current VMG of the boat.
+// * @param potentialVMG The new VMG to test.
+// * @param bearingToDestination The bearing between the boat and its destination.
+// * @return True if the new VMG is improves velocity, false otherwise.
+// */
+// private boolean improvesVelocity(VMG currentVMG, VMG potentialVMG, Bearing bearingToDestination) {
+//
+// //Calculates the angle between the boat and its destination.
+// Angle angleBetweenDestAndHeading = Angle.fromDegrees(currentVMG.getBearing().degrees() - bearingToDestination.degrees());
+//
+// //Calculates the angle between the new VMG and the boat's destination.
+// Angle angleBetweenDestAndNewVMG = Angle.fromDegrees(potentialVMG.getBearing().degrees() - bearingToDestination.degrees());
+//
+//
+// //Calculate the boat's current velocity.
+// double currentVelocity = Math.cos(angleBetweenDestAndHeading.radians()) * currentVMG.getSpeed();
+//
+// //Calculate the potential velocity with the new VMG.
+// double vmgVelocity = Math.cos(angleBetweenDestAndNewVMG.radians()) * potentialVMG.getSpeed();
+//
+// //Return whether or not the new VMG gives better velocity.
+// return vmgVelocity > currentVelocity;
+//
+// }
+//
+// /**
+// * Determines whether or not a given VMG improves the velocity of a boat.
+// * @param boat The boat to test.
+// * @param vmg The new VMG to test.
+// * @return True if the new VMG is improves velocity, false otherwise.
+// */
+// private boolean improvesVelocity(MockBoat boat, VMG vmg) {
+//
+// //Get the boats "current" VMG.
+// VMG boatVMG = new VMG(boat.getCurrentSpeed(), boat.getBearing());
+//
+// //Check if the new VMG is better than the boat's current VMG.
+// return this.improvesVelocity(boatVMG, vmg, boat.calculateBearingToNextMarker());
+//
+// }
+//
+//
+// /**
+// * Calculates the distance a boat has travelled and updates its current position according to this value.
+// *
+// * @param boat The boat to be updated.
+// * @param updatePeriodMilliseconds The time, in milliseconds, since the last update.
+// * @param totalElapsedMilliseconds The total number of milliseconds that have elapsed since the start of the race.
+// */
+// protected void updatePosition(MockBoat boat, long updatePeriodMilliseconds, long totalElapsedMilliseconds) {
+//
+// //Checks if the current boat has finished the race or not.
+// boolean finish = this.isLastLeg(boat.getCurrentLeg());
+//
+// if (!finish) {
+//
+//
+// //Calculates the distance travelled, in meters, in the current timeslice.
+// double distanceTravelledMeters = boat.calculateMetersTravelled(updatePeriodMilliseconds);
+//
+// //Scale it.
+// distanceTravelledMeters = distanceTravelledMeters * this.scaleFactor;
+//
+//
+// //Move the boat forwards that many meters, and advances its time counters by enough milliseconds.
+// boat.moveForwards(distanceTravelledMeters, updatePeriodMilliseconds * this.scaleFactor);
+//
+// long tackPeriod = 15000;
+// if (boat.getTimeSinceTackChange() > tackPeriod) {
+// //Calculate the new VMG.
+// VMG newVMG = this.calculateVMG(boat);
+//
+//
+// //If the new vmg improves velocity, use it.
+// if (improvesVelocity(boat, newVMG)) {
+// boat.setVMG(newVMG);
+//
+// }
+// }
+//
+// this.updateEstimatedTime(boat);
+//
+//
+// //Check the boats position (update leg and stuff).
+// this.checkPosition(boat, totalElapsedMilliseconds);
+//
+// }
+//
+// }
+//
+//
+// /**
+// * Checks if a boat has finished any legs, or has pulled out of race (DNF).
+// * @param boat The boat to check.
+// * @param timeElapsed The total time, in milliseconds, that has elapsed since the race started.
+// */
+// protected void checkPosition(MockBoat boat, long timeElapsed) {
+//
+// //The distance, in nautical miles, within which the boat needs to get in order to consider that it has reached the marker.
+// double epsilonNauticalMiles = 100.0 / Constants.NMToMetersConversion; //100 meters. TODO should be more like 5-10.
+//
+// if (boat.calculateDistanceToNextMarker() < epsilonNauticalMiles) {
+// //Boat has reached its target marker, and has moved on to a new leg.
+//
+//
+//
+// //Calculate how much the boat overshot the marker by.
+// double overshootMeters = boat.calculateDistanceToNextMarker();
+//
+//
+// //Move boat on to next leg.
+// Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1);
+// boat.setCurrentLeg(nextLeg);
+//
+// //Add overshoot distance into the distance travelled for the next leg.
+// boat.setDistanceTravelledInLeg(overshootMeters);
+//
+// //Setting a high value for this allows the boat to immediately do a large turn, as it needs to in order to get to the next mark.
+// boat.setTimeSinceTackChange(999999);
+//
+//
+// //Check if the boat has finished or stopped racing.
+//
+// if (this.isLastLeg(boat.getCurrentLeg())) {
+// //Boat has finished.
+// boat.setTimeFinished(timeElapsed);
+// boat.setCurrentSpeed(0);
+// boat.setStatus(BoatStatusEnum.FINISHED);
+//
+// }
+//
+// }
+//
+// }
+//
+// /**
+// * Updates the boat's estimated time to next mark if positive
+// * @param boat to estimate time given its velocity
+// */
+// private void updateEstimatedTime(MockBoat boat) {
+//
+// double velocityToMark = boat.getCurrentSpeed() * cos(boat.getBearing().radians() - boat.calculateBearingToNextMarker().radians()) / Constants.KnotsToMMPerSecond;
+//
+// if (velocityToMark > 0) {
+//
+// //Calculate milliseconds until boat reaches mark.
+// long timeFromNow = (long) (1000 * boat.calculateDistanceToNextMarker() / velocityToMark);
+//
+// //Calculate time at which it will reach mark.
+// ZonedDateTime timeAtMark = this.raceClock.getCurrentTime().plus(timeFromNow, ChronoUnit.MILLIS);
+// boat.setEstimatedTimeAtNextMark(timeAtMark);
+// }
+//
+// }
+// }
+//}
diff --git a/racevisionGame/src/main/java/mock/model/WindGenerator.java b/racevisionGame/src/main/java/mock/model/WindGenerator.java
index 30fd24b4..8285d5d3 100644
--- a/racevisionGame/src/main/java/mock/model/WindGenerator.java
+++ b/racevisionGame/src/main/java/mock/model/WindGenerator.java
@@ -1,249 +1,29 @@
package mock.model;
-
-import shared.model.Bearing;
import shared.model.Wind;
-import java.util.Random;
/**
- * This class generates Wind objects for use in a MockRace.
- * Bounds on bearing and speed can be specified.
- * Wind can be completely random, or random incremental change.
+ * Interface for wind generators. It allows for generating a baseline wind, and subsequent winds.
*/
-public class WindGenerator {
-
- /**
- * The bearing the wind direction starts at.
- */
- private Bearing windBaselineBearing;
-
- /**
- * The lower bearing angle that the wind may have.
- */
- private Bearing windBearingLowerBound;
-
- /**
- * The upper bearing angle that the wind may have.
- */
- private Bearing windBearingUpperBound;
-
-
-
- /**
- * The speed the wind starts at, in knots.
- */
- private double windBaselineSpeed;
-
- /**
- * The lower speed that the wind may have, in knots.
- */
- private double windSpeedLowerBound;
-
- /**
- * The upper speed that the wind may have, in knots.
- */
- private double windSpeedUpperBound;
-
-
- /**
- * Creates a wind generator, with a baseline, lower bound, and upper bound, for the wind speed and direction.
- * @param windBaselineBearing Baseline wind direction.
- * @param windBearingLowerBound Lower bound for wind direction.
- * @param windBearingUpperBound Upper bound for wind direction.
- * @param windBaselineSpeed Baseline wind speed, in knots.
- * @param windSpeedLowerBound Lower bound for wind speed, in knots.
- * @param windSpeedUpperBound Upper bound for wind speed, in knots.
- */
- public WindGenerator(Bearing windBaselineBearing, Bearing windBearingLowerBound, Bearing windBearingUpperBound, double windBaselineSpeed, double windSpeedLowerBound, double windSpeedUpperBound) {
-
- this.windBaselineBearing = windBaselineBearing;
- this.windBearingLowerBound = windBearingLowerBound;
- this.windBearingUpperBound = windBearingUpperBound;
- this.windBaselineSpeed = windBaselineSpeed;
- this.windSpeedLowerBound = windSpeedLowerBound;
- this.windSpeedUpperBound = windSpeedUpperBound;
+public interface WindGenerator {
- }
/**
* Generates a wind object using the baseline wind speed and bearing.
* @return Baseline wind object.
*/
- public Wind generateBaselineWind() {
- return new Wind(windBaselineBearing, windBaselineSpeed);
- }
-
- /**
- * Generates a random Wind object, that is within the provided bounds.
- * @return Generated wind object.
- */
- public Wind generateRandomWind() {
-
- double windSpeed = generateRandomWindSpeed();
- Bearing windBearing = generateRandomWindBearing();
-
- return new Wind(windBearing, windSpeed);
-
- }
-
- /**
- * Generates a random wind speed within the specified bounds. In knots.
- * @return Wind speed, in knots.
- */
- private double generateRandomWindSpeed() {
-
- double randomSpeedKnots = generateRandomValueInBounds(windSpeedLowerBound, windSpeedUpperBound);
-
- return randomSpeedKnots;
- }
-
-
- /**
- * Generates a random wind bearing within the specified bounds.
- * @return Wind bearing.
- */
- private Bearing generateRandomWindBearing() {
-
- double randomBearingDegrees = generateRandomValueInBounds(windBearingLowerBound.degrees(), windBearingUpperBound.degrees());
-
- return Bearing.fromDegrees(randomBearingDegrees);
- }
-
-
- /**
- * Generates a random value within a specified interval.
- * @param lowerBound The lower bound of the interval.
- * @param upperBound The upper bound of the interval.
- * @return A random value within the interval.
- */
- private static double generateRandomValueInBounds(double lowerBound, double upperBound) {
+ Wind generateBaselineWind();
- float proportion = new Random().nextFloat();
-
- double delta = upperBound - lowerBound;
-
- double amount = delta * proportion;
-
- double finalAmount = amount + lowerBound;
-
- return finalAmount;
-
- }
-
-
- /**
- * Generates a new value within an interval, given a start value, chance to change, and change amount.
- * @param lowerBound Lower bound of interval.
- * @param upperBound Upper bound of interval.
- * @param currentValue The current value to change.
- * @param changeAmount The amount to change by.
- * @param chanceToChange The change to actually change the value.
- * @return The new value.
- */
- private static double generateNextValueInBounds(double lowerBound, double upperBound, double currentValue, double changeAmount, double chanceToChange) {
-
- float chance = new Random().nextFloat();
-
-
- if (chance <= chanceToChange) {
- currentValue += changeAmount;
-
- } else if (chance <= (2 * chanceToChange)) {
- currentValue -= changeAmount;
-
- }
-
- currentValue = clamp(lowerBound, upperBound, currentValue);
-
- return currentValue;
-
- }
/**
- * Generates the next Wind object, that is within the provided bounds. This randomly increases or decreases the wind's speed and bearing.
+ * Generates the next Wind object, according to the implementation of the wind generator.
* @param currentWind The current wind to change. This is not modified.
* @return Generated wind object.
*/
- public Wind generateNextWind(Wind currentWind) {
-
- double windSpeed = generateNextWindSpeed(currentWind.getWindSpeed());
- Bearing windBearing = generateNextWindBearing(currentWind.getWindDirection());
-
- return new Wind(windBearing, windSpeed);
-
- }
-
-
- /**
- * Generates the next wind speed to use.
- * @param windSpeed Current wind speed, in knots.
- * @return Next wind speed, in knots.
- */
- private double generateNextWindSpeed(double windSpeed) {
-
- double chanceToChange = 0.2;
- double changeAmount = 0.1;
-
- double nextWindSpeed = generateNextValueInBounds(
- windSpeedLowerBound,
- windSpeedUpperBound,
- windSpeed,
- changeAmount,
- chanceToChange);
-
- return nextWindSpeed;
- }
-
-
- /**
- * Generates the next wind speed to use.
- * @param windBearing Current wind bearing.
- * @return Next wind speed.
- */
- private Bearing generateNextWindBearing(Bearing windBearing) {
-
- double chanceToChange = 0.2;
- double changeAmount = 0.5;
-
- double nextWindBearingDegrees = generateNextValueInBounds(
- windBearingLowerBound.degrees(),
- windBearingUpperBound.degrees(),
- windBearing.degrees(),
- changeAmount,
- chanceToChange);
-
- return Bearing.fromDegrees(nextWindBearingDegrees);
- }
-
-
-
-
-
- /**
- * Clamps a value to be within an interval.
- * @param lower Lower bound of the interval.
- * @param upper Upper bound of the interval.
- * @param value Value to clamp.
- * @return The clamped value.
- */
- private static double clamp(double lower, double upper, double value) {
-
- if (value > upper) {
- value = upper;
-
- } else if (value < lower) {
- value = lower;
-
- }
-
- return value;
- }
-
-
-
+ Wind generateNextWind(Wind currentWind);
}
diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java b/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java
index fba06cb5..4edb87c5 100644
--- a/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java
+++ b/racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java
@@ -1,8 +1,10 @@
package mock.model.commandFactory;
+import mock.exceptions.CommandConstructionException;
import mock.model.MockBoat;
import mock.model.MockRace;
-import network.Messages.Enums.BoatActionEnum;
+import network.Messages.BoatAction;
+import shared.exceptions.BoatNotFoundException;
/**
* Factory class for Command objects
@@ -11,17 +13,28 @@ public class CommandFactory {
/**
* Generates a command on a race and boat corresponding to the protocol action number.
* @param race to receive command
- * @param boat to receive command in race
* @param action number to select command
- * @return
+ * @return The command to execute the given action.
+ * @throws CommandConstructionException Thrown if the command cannot be constructed (e.g., unknown action type).
*/
- public static Command createCommand(MockRace race, MockBoat boat, BoatActionEnum action) {
- switch(action) {
+ public static Command createCommand(MockRace race, BoatAction action) throws CommandConstructionException {
+
+ MockBoat boat = null;
+ try {
+ boat = race.getBoat(action.getSourceID());
+
+ } catch (BoatNotFoundException e) {
+ throw new CommandConstructionException("Could not create command for BoatAction: " + action + ". Boat with sourceID: " + action.getSourceID() + " not found.", e);
+
+ }
+
+ switch(action.getBoatAction()) {
case AUTO_PILOT: return new VMGCommand(race, boat);
case TACK_GYBE: return new TackGybeCommand(race, boat);
case UPWIND: return new WindCommand(race, boat, true);
case DOWNWIND: return new WindCommand(race, boat, false);
- default: return null; // TODO - please please have discussion over what to default to
+
+ default: throw new CommandConstructionException("Could not create command for BoatAction: " + action + ". Unknown BoatAction.");
}
}
}
diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java
index 150a1da8..d0b0584b 100644
--- a/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java
+++ b/racevisionGame/src/main/java/mock/model/commandFactory/TackGybeCommand.java
@@ -2,31 +2,52 @@ package mock.model.commandFactory;
import mock.model.MockBoat;
import mock.model.MockRace;
+import shared.model.Bearing;
/**
- * Created by David on 2/08/2017.
+ * Command class for tacking and gybing
*/
public class TackGybeCommand implements Command {
private MockRace race;
private MockBoat boat;
+ /**
+ * Constructor for class
+ * @param race mock race
+ * @param boat mock boat to update
+ */
public TackGybeCommand(MockRace race, MockBoat boat) {
this.race = race;
this.boat = boat;
}
- //The refactoring of MockRace will require changes to be made
@Override
public void execute() {
- /*VMG newVMG = boat.getPolars().calculateVMG(
- race.getWindDirection(),
- race.getWindSpeed(),
- boat.calculateBearingToNextMarker(),
- Bearing.fromDegrees(0d),
- Bearing.fromDegrees(359.99999d));
- VMG boatVMG = new VMG(boat.getCurrentSpeed(), boat.getBearing());
- if(race.improvesVelocity(boatVMG, newVMG, boat.calculateBearingToNextMarker())){
- boat.setVMG(newVMG);
- }*/
+
+ boat.setAutoVMG(false);
+
+ double boatAngle = boat.getBearing().degrees();
+ double windAngle =race.getWindDirection().degrees();
+ double differenceAngle = calcDistance(boatAngle, windAngle);
+ double angleA = windAngle + differenceAngle;
+ double angleB = windAngle - differenceAngle;
+ if(angleA % 360 == boatAngle){
+ boat.setBearing(Bearing.fromDegrees(angleB));
+ } else {
+ boat.setBearing(Bearing.fromDegrees(angleA));
+ }
+ }
+
+ /**
+ * Method to calculate smallest angle between 2 angles
+ * @param degreeA first angle degree
+ * @param degreeB second angle degree
+ * @return the calculated smallest angle
+ */
+ public double calcDistance(double degreeA, double degreeB){
+ double phi = Math.abs(degreeB - degreeA) % 360;
+ return phi > 180 ? 360 - phi : phi;
}
+
}
+
diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java
index 64cc6a9f..39469cf8 100644
--- a/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java
+++ b/racevisionGame/src/main/java/mock/model/commandFactory/VMGCommand.java
@@ -4,26 +4,28 @@ import mock.model.MockBoat;
import mock.model.MockRace;
/**
- * Created by David on 2/08/2017.
+ * Command class for autoVMG
*/
public class VMGCommand implements Command {
private MockRace race;
private MockBoat boat;
+ /**
+ * Constructor for class
+ * @param race mock race
+ * @param boat mock boat to update
+ */
public VMGCommand(MockRace race, MockBoat boat) {
this.race = race;
this.boat = boat;
}
- //The refactoring of MockRace will require changes to be made
@Override
public void execute() {
- /*VMG newVMG = boat.getPolars().calculateVMG(
- race.getWindDirection(),
- race.getWindSpeed(),
- boat.calculateBearingToNextMarker(),
- Bearing.fromDegrees(0d),
- Bearing.fromDegrees(359.99999d));
- boat.setVMG(newVMG);*/
+ if (boat.getAutoVMG()){
+ boat.setAutoVMG(false);
+ } else {
+ boat.setAutoVMG(true);
+ }
}
}
diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/WindCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/WindCommand.java
index 5ba5d5bf..530bf5bc 100644
--- a/racevisionGame/src/main/java/mock/model/commandFactory/WindCommand.java
+++ b/racevisionGame/src/main/java/mock/model/commandFactory/WindCommand.java
@@ -20,13 +20,17 @@ public class WindCommand implements Command {
@Override
public void execute() {
+
+ boat.setAutoVMG(false);
+
double wind = race.getWindDirection().degrees();
double heading = boat.getBearing().degrees();
double offset = 3.0;
offset *= direction;
- if(wind - heading < 0) offset *= -1;
+ double headWindDelta = wind - heading;
+ if ((headWindDelta < 0) || (headWindDelta > 180)) offset *= -1;
boat.setBearing(Bearing.fromDegrees(heading + offset));
}
diff --git a/racevisionGame/src/main/java/network/BinaryMessageDecoder.java b/racevisionGame/src/main/java/network/BinaryMessageDecoder.java
index ecc6b2f3..7bd9c233 100644
--- a/racevisionGame/src/main/java/network/BinaryMessageDecoder.java
+++ b/racevisionGame/src/main/java/network/BinaryMessageDecoder.java
@@ -2,6 +2,7 @@ package network;
import network.Exceptions.InvalidMessageException;
+import network.Exceptions.InvalidMessageTypeException;
import network.MessageDecoders.*;
import network.Messages.*;
import network.Messages.Enums.MessageType;
@@ -16,42 +17,71 @@ import java.util.zip.CRC32;
*/
public class BinaryMessageDecoder {
- ///Length of the header.
+ /**
+ * Length of the header.
+ */
private static final int headerLength = 15;
- ///Length of the CRC.
- private static final int CRCLength = 4;//TODO these should probably be static defined somewhere else to be shared.
+ /**
+ * Length of the CRC.
+ */
+ private static final int CRCLength = 4;
- ///The value the first sync byte should have.
+ /**
+ * The value the first sync byte should have.
+ */
private static final byte syncByte1 = (byte) 0x47;
- //The value the second sync byte should have.
+ /**
+ * The value the second sync byte should have.
+ */
private static final byte syncByte2 = (byte) 0x83;
- ///The full message.
+ /**
+ * The full message.
+ */
private byte[] fullMessage;
- ///The messageHeader.
+ /**
+ * The messageHeader.
+ */
private byte[] messageHeader;
- ///The messageBody.
+ /**
+ * The messageBody.
+ */
private byte[] messageBody;
- ///The sync bytes from the header..
+ /**
+ * The sync bytes from the header.
+ */
private byte headerSync1;
private byte headerSync2;
- ///The message type from the header.
+ /**
+ * The message type from the header.
+ */
private byte headerMessageType;
- ///The timestamp from the header.
+ /**
+ * The timestamp from the header.
+ */
private long headerTimeStamp;
- ///The source ID from the header.
+ /**
+ * The source ID from the header.
+ */
private int headerSourceID;
- ///The message body length from the header.
+ /**
+ * The message body length from the header.
+ */
private int messageBodyLength;
- ///CRC value read from message header.
+ /**
+ * CRC value read from message header.
+ */
private long messageCRCValue;
- ///Calculated CRC value from message.
+
+ /**
+ * Calculated CRC value from message.
+ */
private long calculatedCRCValue;
@@ -113,96 +143,34 @@ public class BinaryMessageDecoder {
//Check the message body length.
throw new InvalidMessageException("MessageBody length in header does not equal the messageBody length. MessageBody length in header is: " + messageBodyLength + ", should be: " + messageBody.length);
- }else if (headerSync1 != syncByte1) {
+ } else if (headerSync1 != syncByte1) {
//Check the first sync byte.
throw new InvalidMessageException("Sync byte 1 is wrong. Sync byte is: " + headerSync1 + ", should be: " + syncByte1);
- }else if (headerSync2 != syncByte2) {
+ } else if (headerSync2 != syncByte2) {
//Check the second sync byte.
throw new InvalidMessageException("Sync byte 2 is wrong. Sync byte is: " + headerSync2 + ", should be: " + syncByte2);
- }else if (calculatedCRCValue != messageCRCValue) {
+ } else if (calculatedCRCValue != messageCRCValue) {
//Check the CRC value.
throw new InvalidMessageException("CRC value is wrong. The calculated value is: " + calculatedCRCValue + ", should be: " + messageCRCValue);
}
//Now we create the message object based on what is actually in the message body.
- MessageType mType = MessageType.fromByte(headerMessageType);
-
- switch(mType) {
- case HEARTBEAT:
- //System.out.println("Decoding HeartBeat Message!");
- //TODO maybe use HeartbeatDecoder.decode(message).
- //TODO also, decoders for each message type should encapsulate the constructing of the object. E.g., return HeartbeatDecoder.decode(message);.
- return new Heartbeat(bytesToLong(messageBody));
-
- case RACESTATUS:
- //System.out.println("Race Status Message");
- RaceStatusDecoder rsdecoder = new RaceStatusDecoder(messageBody);
- return new RaceStatus(rsdecoder.getTime(), rsdecoder.getRace(), rsdecoder.getRaceState(), rsdecoder.getStartTime(), rsdecoder.getRaceWindDir(), rsdecoder.getRaceWindSpeed(), rsdecoder.getRaceType(), rsdecoder.getBoats());
-
- case DISPLAYTEXTMESSAGE:
- //System.out.println("Display Text Message");
- //No decoder for this.
- //throw new InvalidMessageException("Cannot decode DISPLAYTEXTMESSAGE - no decoder.");
-
- case XMLMESSAGE:
- //System.out.println("XML Message!");
- XMLMessageDecoder xmdecoder = new XMLMessageDecoder(messageBody);
- xmdecoder.decode();
- return new XMLMessage(XMLMessage.currentVersionNumber, xmdecoder.getAckNumber(), xmdecoder.getTimeStamp(), xmdecoder.getXmlMsgSubType(), xmdecoder.getSequenceNumber(), xmdecoder.getXmlMessageContents());
-
- case RACESTARTSTATUS:
- //System.out.println("Race Start Status Message");
- RaceStartStatusDecoder rssDecoder = new RaceStartStatusDecoder(messageBody);
- return new RaceStartStatus(rssDecoder.getTime(), rssDecoder.getAck(), rssDecoder.getStartTime(), rssDecoder.getRaceID(), rssDecoder. getNotification());
-
- case YACHTEVENTCODE:
- //System.out.println("Yacht Action Code!");
- //No decoder for this.
- //throw new InvalidMessageException("Cannot decode YACHTEVENTCODE - no decoder.");
-
- case YACHTACTIONCODE:
- //System.out.println("Yacht Action Code!");
- //No decoder for this.
- //throw new InvalidMessageException("Cannot decode YACHTACTIONCODE - no decoder.");
-
- case CHATTERTEXT:
- //System.out.println("Chatter Text Message!");
- //No decoder for this.
- //throw new InvalidMessageException("Cannot decode CHATTERTEXT - no decoder.");
-
- case BOATLOCATION:
- //System.out.println("Boat Location Message!");
- BoatLocationDecoder blDecoder = new BoatLocationDecoder(messageBody);
- return blDecoder.getMessage();
-
- case MARKROUNDING:
- //System.out.println("Mark Rounding Message!");
- MarkRoundingDecoder mrDecoder = new MarkRoundingDecoder(messageBody);
- return mrDecoder.getMarkRounding();
-
- case COURSEWIND:
- //System.out.println("Course Wind Message!");
- CourseWindDecoder cwDecoder = new CourseWindDecoder(messageBody);
- return new CourseWinds(cwDecoder.getMessageVersionNumber(), cwDecoder.getByteWindID(), cwDecoder.getLoopMessages());
-
- case AVGWIND:
- //System.out.println("Average Wind Message!");
- AverageWindDecoder awDecoder = new AverageWindDecoder(messageBody);
- return awDecoder.getAverageWind();
-
- case BOATACTION:
- BoatActionDecoder baDecoder = new BoatActionDecoder(messageBody);
- return new BoatAction(baDecoder.getBoatAction());
-
- default:
- //System.out.println("Broken Message!");
- //throw new InvalidMessageException("Broken message! Did not recognise message type: " + headerMessageType + ".");
- return null;
+ MessageType messageType = MessageType.fromByte(headerMessageType);
+
+ MessageDecoder decoder = null;
+ try {
+ decoder = DecoderFactory.create(messageType);
+
+ } catch (InvalidMessageTypeException e) {
+ throw new InvalidMessageException("Could not create decoder for MessageType: " + messageType, e);
}
+
+ return decoder.decode(messageBody);
+
}
diff --git a/racevisionGame/src/main/java/network/Exceptions/InvalidMessageTypeException.java b/racevisionGame/src/main/java/network/Exceptions/InvalidMessageTypeException.java
new file mode 100644
index 00000000..02867eaa
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Exceptions/InvalidMessageTypeException.java
@@ -0,0 +1,17 @@
+package network.Exceptions;
+
+
+/**
+ * An exception thrown when we encounter a message type that isn't recognised.
+ */
+public class InvalidMessageTypeException extends Exception {
+
+
+ public InvalidMessageTypeException(String message) {
+ super(message);
+ }
+
+ public InvalidMessageTypeException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageControllers/MessageController.java b/racevisionGame/src/main/java/network/MessageControllers/MessageController.java
new file mode 100644
index 00000000..7b6cca14
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageControllers/MessageController.java
@@ -0,0 +1,9 @@
+package network.MessageControllers;
+
+
+
+public class MessageController {
+
+
+
+}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java
index 13de3c80..139aed13 100644
--- a/racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java
+++ b/racevisionGame/src/main/java/network/MessageDecoders/AverageWindDecoder.java
@@ -1,56 +1,107 @@
package network.MessageDecoders;
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
import network.Messages.AverageWind;
import network.Utils.ByteConverter;
+import static network.Utils.AC35UnitConverter.*;
+
import java.util.Arrays;
/**
- * Created by hba56 on 23/04/17.
+ * Decodes {@link AverageWind} messages.
*/
-public class AverageWindDecoder {
- byte messageVersionNumber;
- byte[] byteTime;
- byte[] byteRawPeriod;
- byte[] byteRawSpeed;
- byte[] bytePeriod2;
- byte[] byteSpeed2;
- byte[] bytePeriod3;
- byte[] byteSpeed3;
- byte[] bytePeriod4;
- byte[] byteSpeed4;
-
- AverageWind averageWind;
-
- public AverageWindDecoder(byte[] encodedAverageWind) {
- messageVersionNumber = encodedAverageWind[0];
- byteTime = Arrays.copyOfRange(encodedAverageWind, 1, 7);
- byteRawPeriod = Arrays.copyOfRange(encodedAverageWind, 7, 9);
- byteRawSpeed = Arrays.copyOfRange(encodedAverageWind, 9, 11);
- bytePeriod2 = Arrays.copyOfRange(encodedAverageWind, 11, 13);
- byteSpeed2 = Arrays.copyOfRange(encodedAverageWind, 13, 15);
- bytePeriod3 = Arrays.copyOfRange(encodedAverageWind, 15, 17);
- byteSpeed3 = Arrays.copyOfRange(encodedAverageWind, 17, 19);
- bytePeriod4 = Arrays.copyOfRange(encodedAverageWind, 19, 21);
- byteSpeed4 = Arrays.copyOfRange(encodedAverageWind, 21, 23);
-
- int msgNum = ByteConverter.bytesToInt(messageVersionNumber);
- long lngTime = ByteConverter.bytesToLong(byteTime);
- int intRawPeriod = ByteConverter.bytesToInt(byteRawPeriod);
- int intRawSpeed = ByteConverter.bytesToInt(byteRawSpeed);
- int intPeriod2 = ByteConverter.bytesToInt(bytePeriod2);
- int intSpeed2 = ByteConverter.bytesToInt(byteSpeed2);
- int intPeriod3 = ByteConverter.bytesToInt(bytePeriod3);
- int intSpeed3 = ByteConverter.bytesToInt(byteSpeed3);
- int intPeriod4 = ByteConverter.bytesToInt(bytePeriod4);
- int intSpeed4 = ByteConverter.bytesToInt(byteSpeed4);
-
- this.averageWind = new AverageWind(msgNum, lngTime, intRawPeriod, intRawSpeed, intPeriod2, intSpeed2, intPeriod3, intSpeed3, intPeriod4, intSpeed4);
+public class AverageWindDecoder implements MessageDecoder {
+
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
+
+ /**
+ * The decoded message.
+ */
+ private AverageWind message;
+
+
+
+ public AverageWindDecoder() {
+ }
+
+
+
+ @Override
+ public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
+
+ try {
+
+ byte messageVersionNumber = encodedMessage[0];
+
+
+ byte[] byteTime = Arrays.copyOfRange(encodedMessage, 1, 7);
+ long time = ByteConverter.bytesToLong(byteTime);
+
+ byte[] byteRawPeriod = Arrays.copyOfRange(encodedMessage, 7, 9);
+ int intRawPeriod = ByteConverter.bytesToInt(byteRawPeriod);
+ long rawPeriod = unpackAverageWindPeriod(intRawPeriod);
+
+ byte[] byteRawSpeed = Arrays.copyOfRange(encodedMessage, 9, 11);
+ int intRawSpeed = ByteConverter.bytesToInt(byteRawSpeed);
+ double rawSpeedKnots = unpackMMperSecToKnots(intRawSpeed);
+
+ byte[] bytePeriod2 = Arrays.copyOfRange(encodedMessage, 11, 13);
+ int intPeriod2 = ByteConverter.bytesToInt(bytePeriod2);
+ long period2 = unpackAverageWindPeriod(intPeriod2);
+
+ byte[] byteSpeed2 = Arrays.copyOfRange(encodedMessage, 13, 15);
+ int intSpeed2 = ByteConverter.bytesToInt(byteSpeed2);
+ double speed2Knots = unpackMMperSecToKnots(intSpeed2);
+
+ byte[] bytePeriod3 = Arrays.copyOfRange(encodedMessage, 15, 17);
+ int intPeriod3 = ByteConverter.bytesToInt(bytePeriod3);
+ long period3 = unpackAverageWindPeriod(intPeriod3);
+
+ byte[] byteSpeed3 = Arrays.copyOfRange(encodedMessage, 17, 19);
+ int intSpeed3 = ByteConverter.bytesToInt(byteSpeed3);
+ double speed3Knots = unpackMMperSecToKnots(intSpeed3);
+
+ byte[] bytePeriod4 = Arrays.copyOfRange(encodedMessage, 19, 21);
+ int intPeriod4 = ByteConverter.bytesToInt(bytePeriod4);
+ long period4 = unpackAverageWindPeriod(intPeriod4);
+
+ byte[] byteSpeed4 = Arrays.copyOfRange(encodedMessage, 21, 23);
+ int intSpeed4 = ByteConverter.bytesToInt(byteSpeed4);
+ double speed4Knots = unpackMMperSecToKnots(intSpeed4);
+
+
+ message = new AverageWind(
+ messageVersionNumber,
+ time,
+ rawPeriod,
+ rawSpeedKnots,
+ period2,
+ speed2Knots,
+ period3,
+ speed3Knots,
+ period4,
+ speed4Knots);
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode AverageWind message.", e);
+ }
}
- public AverageWind getAverageWind() {
- return averageWind;
+ /**
+ * Returns the decoded message.
+ * @return The decoded message.
+ */
+ public AverageWind getMessage() {
+ return message;
}
}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/BoatActionDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/BoatActionDecoder.java
index bf2076b5..1df1e2f0 100644
--- a/racevisionGame/src/main/java/network/MessageDecoders/BoatActionDecoder.java
+++ b/racevisionGame/src/main/java/network/MessageDecoders/BoatActionDecoder.java
@@ -1,20 +1,56 @@
package network.MessageDecoders;
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.BoatAction;
import network.Messages.Enums.BoatActionEnum;
import java.util.Arrays;
-public class BoatActionDecoder {
- byte byteBoatAction;
- BoatActionEnum boatAction;
+/**
+ * Decodes {@link BoatAction} messages.
+ */
+public class BoatActionDecoder implements MessageDecoder {
- public BoatActionDecoder(byte[] encodedBoatAction) {
- byteBoatAction = encodedBoatAction[0];
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
- boatAction = BoatActionEnum.fromByte(byteBoatAction);
+ /**
+ * The decoded message.
+ */
+ private BoatAction message;
+
+
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public BoatActionDecoder() {
}
- public BoatActionEnum getBoatAction() {
- return boatAction;
+ @Override
+ public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
+
+ try {
+ BoatActionEnum boatActionEnum = BoatActionEnum.fromByte(encodedMessage[0]);
+
+ message = new BoatAction(boatActionEnum);
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode BoatAction message.", e);
+ }
+ }
+
+
+ /**
+ * Returns the decoded message.
+ * @return The decoded message.
+ */
+ public BoatAction getMessage() {
+ return message;
}
-}
\ No newline at end of file
+}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java
index db90a343..2965e809 100644
--- a/racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java
+++ b/racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java
@@ -1,142 +1,168 @@
package network.MessageDecoders;
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
import network.Messages.BoatLocation;
+import network.Messages.Enums.BoatLocationDeviceEnum;
+import shared.model.Azimuth;
+import shared.model.Bearing;
import java.util.Arrays;
-import static network.Utils.ByteConverter.bytesToInt;
-import static network.Utils.ByteConverter.bytesToLong;
-import static network.Utils.ByteConverter.bytesToShort;
+import static network.Utils.AC35UnitConverter.*;
+import static network.Utils.ByteConverter.*;
/**
- * Created by hba56 on 21/04/17.
+ * Decodes {@link BoatLocation} messages.
*/
-public class BoatLocationDecoder {
- private byte messageVersionNumber;
- private byte[] time;
- private byte[] sourceID;
- private byte[] seqNum;
- private byte deviceType;
- private byte[] latitude;
- private byte[] longitude;
- private byte[] altitude;
- private byte[] heading;
- private byte[] pitch;
- private byte[] roll;
- private byte[] boatSpeed;
- private byte[] cog;
- private byte[] sog;
- private byte[] apparentWindSpeed;
- private byte[] apparentWindAngle;
- private byte[] trueWindSpeed;
- private byte[] trueWindDirection;
- private byte[] trueWindAngle;
- private byte[] currentDrift;
- private byte[] currentSet;
- private byte[] rudderAngle;
+public class BoatLocationDecoder implements MessageDecoder {
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
+
+ /**
+ * The decoded message.
+ */
private BoatLocation message;
- public BoatLocationDecoder(byte[] encodedBoatLocation) {
- byte numMessageVersionNumber = 0;
- long numTime = 0;
- int numSourceID = 0;
- int numSeqNum = 0;
- byte numDeviceType = 0;
- int numLatitude = 0;
- int numLongitude = 0;
- int numAltitude = 0;
- int numHeading = 0;
- short numPitch = 0;
- short numRoll = 0;
- int numBoatSpeed = 0;
- int numCog = 0;
- int numSog = 0;
- int numApparentWindSpeed = 0;
- short numApparentWindAngle = 0;
- int numTrueWindSpeed = 0;
- short numTrueWindDirection = 0;
- short numTrueWindAngle = 0;
- int numCurrentDrift = 0;
- int numCurrentSet = 0;
- short numRudderAngle = 0;
+
+
+
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public BoatLocationDecoder() {
+ }
+
+
+ @Override
+ public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
try {
- messageVersionNumber = encodedBoatLocation[0];
- numMessageVersionNumber = messageVersionNumber;
- time = Arrays.copyOfRange(encodedBoatLocation, 1, 7);
- numTime = bytesToLong(time);
- sourceID = Arrays.copyOfRange(encodedBoatLocation, 7, 11);
- numSourceID = bytesToInt(sourceID);
- seqNum = Arrays.copyOfRange(encodedBoatLocation, 11, 15);
- numSeqNum = bytesToInt(seqNum);
- deviceType = encodedBoatLocation[15];
- numDeviceType = deviceType;
- latitude = Arrays.copyOfRange(encodedBoatLocation, 16, 20);
- numLatitude = bytesToInt(latitude);
- longitude = Arrays.copyOfRange(encodedBoatLocation, 20, 24);
- numLongitude = bytesToInt(longitude);
- altitude = Arrays.copyOfRange(encodedBoatLocation, 24, 28);
- numAltitude = bytesToInt(altitude);
- heading = Arrays.copyOfRange(encodedBoatLocation, 28, 30);
- numHeading = bytesToInt(heading);
- pitch = Arrays.copyOfRange(encodedBoatLocation, 30, 32);
- numPitch = bytesToShort(pitch);
- roll = Arrays.copyOfRange(encodedBoatLocation, 32, 34);
- numRoll = bytesToShort(roll);
- boatSpeed = Arrays.copyOfRange(encodedBoatLocation, 34, 36);
- numBoatSpeed = bytesToInt(boatSpeed);
- cog = Arrays.copyOfRange(encodedBoatLocation, 36, 38);
- numCog = bytesToInt(cog);
- sog = Arrays.copyOfRange(encodedBoatLocation, 38, 40);
- numSog = bytesToInt(sog);
- apparentWindSpeed = Arrays.copyOfRange(encodedBoatLocation, 40, 42);
- numApparentWindSpeed = bytesToInt(apparentWindSpeed);
- apparentWindAngle = Arrays.copyOfRange(encodedBoatLocation, 42, 44);
- numApparentWindAngle = bytesToShort(apparentWindAngle);
- trueWindSpeed = Arrays.copyOfRange(encodedBoatLocation, 44, 46);
- numTrueWindSpeed = bytesToInt(trueWindSpeed);
- trueWindDirection = Arrays.copyOfRange(encodedBoatLocation, 46, 48);
- numTrueWindDirection = bytesToShort(trueWindDirection);
- trueWindAngle = Arrays.copyOfRange(encodedBoatLocation, 48, 50);
- numTrueWindAngle = bytesToShort(trueWindAngle);
- currentDrift = Arrays.copyOfRange(encodedBoatLocation, 50, 52);
- numCurrentDrift = bytesToInt(currentDrift);
- currentSet = Arrays.copyOfRange(encodedBoatLocation, 52, 54);
- numCurrentSet = bytesToShort(currentSet);
- rudderAngle = Arrays.copyOfRange(encodedBoatLocation, 54, 56);
- numRudderAngle = bytesToShort(rudderAngle);
- } catch(ArrayIndexOutOfBoundsException e){
+ byte[] messageVersionNumberBytes = Arrays.copyOfRange(encodedMessage, 0, 1);
+ byte messageVersionNumber = messageVersionNumberBytes[0];
+
+ byte[] timeBytes = Arrays.copyOfRange(encodedMessage, 1, 7);
+ long time = bytesToLong(timeBytes);
+
+ byte[] sourceIDBytes = Arrays.copyOfRange(encodedMessage, 7, 11);
+ int sourceID = bytesToInt(sourceIDBytes);
+
+ byte[] seqNumBytes = Arrays.copyOfRange(encodedMessage, 11, 15);
+ int seqNum = bytesToInt(seqNumBytes);
+
+ byte[] deviceTypeBytes = Arrays.copyOfRange(encodedMessage, 15, 16);
+ BoatLocationDeviceEnum deviceType = BoatLocationDeviceEnum.fromByte(deviceTypeBytes[0]);
+
+ byte[] latitudeBytes = Arrays.copyOfRange(encodedMessage, 16, 20);
+ int numLatitude = bytesToInt(latitudeBytes);
+ double latitude = unpackGPS(numLatitude);
+
+ byte[] longitudeBytes = Arrays.copyOfRange(encodedMessage, 20, 24);
+ int numLongitude = bytesToInt(longitudeBytes);
+ double longitude = unpackGPS(numLongitude);
+
+ byte[] altitudeBytes = Arrays.copyOfRange(encodedMessage, 24, 28);
+ int numAltitude = bytesToInt(altitudeBytes);
+
+ byte[] headingBytes = Arrays.copyOfRange(encodedMessage, 28, 30);
+ int numHeading = bytesToInt(headingBytes);
+ Bearing heading = Bearing.fromDegrees(unpackHeading(numHeading));
+
+ byte[] pitchBytes = Arrays.copyOfRange(encodedMessage, 30, 32);
+ short numPitch = bytesToShort(pitchBytes);
+
+ byte[] rollBytes = Arrays.copyOfRange(encodedMessage, 32, 34);
+ short numRoll = bytesToShort(rollBytes);
+
+ byte[] boatSpeedBytes = Arrays.copyOfRange(encodedMessage, 34, 36);
+ int numBoatSpeed = bytesToInt(boatSpeedBytes);
+ double boatSpeedKnots = unpackMMperSecToKnots(numBoatSpeed);
+
+ byte[] cogBytes = Arrays.copyOfRange(encodedMessage, 36, 38);
+ int numCog = bytesToInt(cogBytes);
+ Bearing cog = Bearing.fromDegrees(unpackHeading(numCog));
+
+ byte[] sogBytes = Arrays.copyOfRange(encodedMessage, 38, 40);
+ int numSog = bytesToInt(sogBytes);
+ double sogKnots = unpackMMperSecToKnots(numSog);
+
+ byte[] apparentWindSpeedBytes = Arrays.copyOfRange(encodedMessage, 40, 42);
+ int numApparentWindSpeed = bytesToInt(apparentWindSpeedBytes);
+ double apparentWindSpeedKnots = unpackMMperSecToKnots(numApparentWindSpeed);
+
+ byte[] apparentWindAngleBytes = Arrays.copyOfRange(encodedMessage, 42, 44);
+ short numApparentWindAngle = bytesToShort(apparentWindAngleBytes);
+ Azimuth apparentWindAngle = Azimuth.fromDegrees(unpackTrueWindAngle(numApparentWindAngle));
+
+ byte[] trueWindSpeedBytes = Arrays.copyOfRange(encodedMessage, 44, 46);
+ int numTrueWindSpeed = bytesToInt(trueWindSpeedBytes);
+ double trueWindSpeedKnots = unpackMMperSecToKnots(numTrueWindSpeed);
+
+ byte[] trueWindDirectionBytes = Arrays.copyOfRange(encodedMessage, 46, 48);
+ short numTrueWindDirection = bytesToShort(trueWindDirectionBytes);
+ Bearing trueWindDirection = Bearing.fromDegrees(unpackHeading(numTrueWindDirection));
+
+ byte[] trueWindAngleBytes = Arrays.copyOfRange(encodedMessage, 48, 50);
+ short numTrueWindAngle = bytesToShort(trueWindAngleBytes);
+ Azimuth trueWindAngle = Azimuth.fromDegrees(unpackTrueWindAngle(numTrueWindAngle));
+
+ byte[] currentDriftBytes = Arrays.copyOfRange(encodedMessage, 50, 52);
+ int numCurrentDrift = bytesToInt(currentDriftBytes);
+ double currentDriftKnots = unpackMMperSecToKnots(numCurrentDrift);
+
+ byte[] currentSetBytes = Arrays.copyOfRange(encodedMessage, 52, 54);
+ int numCurrentSet = bytesToShort(currentSetBytes);
+ Bearing currentSet = Bearing.fromDegrees(unpackHeading(numCurrentSet));
+
+ byte[] rudderAngleBytes = Arrays.copyOfRange(encodedMessage, 54, 56);
+ short numRudderAngle = bytesToShort(rudderAngleBytes);
+ Azimuth rudderAngle = Azimuth.fromDegrees(unpackTrueWindAngle(numRudderAngle));
+
+
+ message = new BoatLocation(
+ messageVersionNumber,
+ time,
+ sourceID,
+ seqNum,
+ deviceType,
+ latitude,
+ longitude,
+ numAltitude,
+ heading,
+ numPitch,
+ numRoll,
+ boatSpeedKnots,
+ cog,
+ sogKnots,
+ apparentWindSpeedKnots,
+ apparentWindAngle,
+ trueWindSpeedKnots,
+ trueWindDirection,
+ trueWindAngle,
+ currentDriftKnots,
+ currentSet,
+ rudderAngle);
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode BoatLocation message.", e);
}
- message = new BoatLocation(numMessageVersionNumber, numTime,
- numSourceID, numSeqNum, numDeviceType, numLatitude,
- numLongitude, numAltitude, numHeading, numPitch,
- numRoll, numBoatSpeed, numCog, numSog, numApparentWindSpeed,
- numApparentWindAngle, numTrueWindSpeed, numTrueWindDirection,
- numTrueWindAngle, numCurrentDrift, numCurrentSet, numRudderAngle
- );/*
- message = new BoatLocation(messageVersionNumber, bytesToLong(time),
- bytesToInt(sourceID), bytesToInt(seqNum),
- deviceType, bytesToInt(latitude),
- bytesToInt(longitude), bytesToInt(altitude),
- bytesToInt(heading), bytesToShort(pitch),
- bytesToShort(roll), bytesToInt(boatSpeed),
- bytesToInt(cog), bytesToInt(sog),
- bytesToInt(apparentWindSpeed), bytesToShort(apparentWindAngle),
- bytesToInt(trueWindSpeed), bytesToShort(trueWindDirection),
- bytesToShort(trueWindAngle), bytesToInt(currentDrift),
- bytesToInt(currentSet), bytesToShort(rudderAngle)
- );*/
-
-// System.out.println(bytesToInt(sourceID));
-// System.out.println(bytesToInt(boatSpeed));
}
+ /**
+ * Returns the decoded message.
+ * @return The decoded message.
+ */
public BoatLocation getMessage() {
return message;
}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/BoatStatusDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/BoatStatusDecoder.java
new file mode 100644
index 00000000..27b88a39
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageDecoders/BoatStatusDecoder.java
@@ -0,0 +1,98 @@
+package network.MessageDecoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.BoatStatus;
+import network.Messages.Enums.BoatStatusEnum;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static network.Utils.ByteConverter.*;
+
+
+/**
+ * Decodes {@link BoatStatus} messages.
+ */
+public class BoatStatusDecoder {
+
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
+
+ /**
+ * The decoded message.
+ */
+ private BoatStatus message;
+
+
+
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public BoatStatusDecoder() {
+ }
+
+
+ /**
+ * Decodes the contained message.
+ * @param encodedMessage The message to decode.
+ * @return The decoded message.
+ * @throws InvalidMessageException Thrown if the encoded message is invalid in some way, or cannot be decoded.
+ */
+ public BoatStatus decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
+
+ try {
+
+ byte[] sourceIDBytes = Arrays.copyOfRange(encodedMessage, 0, 4);
+ int sourceID = bytesToInt(sourceIDBytes);
+
+ byte[] boatStatusBytes = Arrays.copyOfRange(encodedMessage, 4, 5);
+ BoatStatusEnum boatStatus = BoatStatusEnum.fromByte(boatStatusBytes[0]);
+
+ byte[] legNumberBytes = Arrays.copyOfRange(encodedMessage, 5, 6);
+ byte legNumber = legNumberBytes[0];
+
+ byte[] numPenaltiesAwardedBytes = Arrays.copyOfRange(encodedMessage, 6, 7);
+ byte numPenaltiesAwarded = numPenaltiesAwardedBytes[0];
+
+ byte[] numPenaltiesServedBytes = Arrays.copyOfRange(encodedMessage, 7, 8);
+ byte numPenaltiesServed = numPenaltiesServedBytes[0];
+
+ byte[] estTimeAtNextMarkBytes = Arrays.copyOfRange(encodedMessage, 8, 14);
+ long estTimeAtNextMark = bytesToLong(estTimeAtNextMarkBytes);
+
+ byte[] estTimeAtFinishBytes = Arrays.copyOfRange(encodedMessage, 14, 20);
+ long estTimeAtFinish = bytesToLong(estTimeAtFinishBytes);
+
+ message = new BoatStatus(
+ sourceID,
+ boatStatus,
+ legNumber,
+ numPenaltiesAwarded,
+ numPenaltiesServed,
+ estTimeAtNextMark,
+ estTimeAtFinish );
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode BoatStatus message.", e);
+ }
+
+
+ }
+
+
+
+ /**
+ * Returns the decoded message.
+ * @return The decoded message.
+ */
+ public BoatStatus getMessage() {
+ return message;
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java
index 038a79d2..4941df10 100644
--- a/racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java
+++ b/racevisionGame/src/main/java/network/MessageDecoders/CourseWindDecoder.java
@@ -1,64 +1,102 @@
package network.MessageDecoders;
+import network.Exceptions.InvalidMessageException;
import network.Messages.CourseWind;
+import shared.model.Bearing;
-import java.util.ArrayList;
import java.util.Arrays;
-import static network.Utils.ByteConverter.bytesToInt;
-import static network.Utils.ByteConverter.bytesToLong;
+import static network.Utils.AC35UnitConverter.*;
+import static network.Utils.ByteConverter.*;
/**
- * Created by hba56 on 23/04/17.
+ * Decodes {@link CourseWind} messages.
*/
public class CourseWindDecoder {
- byte messageVersionNumber;
- byte byteWindID;
- byte loopCount;
- ArrayList loopMessages = new ArrayList();
-
- public CourseWindDecoder(byte[] encodedCourseWind) {
- final int lengthInBytesOfMessages = 20;
-
- messageVersionNumber = encodedCourseWind[0];
- byteWindID = encodedCourseWind[1];
- loopCount = encodedCourseWind[2];
- byte[] loopMessagesBytes = Arrays.copyOfRange(encodedCourseWind, 3, lengthInBytesOfMessages*loopCount+3);
- int messageLoopIndex = 0;
-
- for (int i=0; i < loopCount; i++) {
- byte[] messageBytes = Arrays.copyOfRange(loopMessagesBytes, messageLoopIndex, messageLoopIndex+20);
- ArrayList test = new ArrayList();
- byte[] windId = Arrays.copyOfRange(messageBytes, 0, 1);
- byte[] time = Arrays.copyOfRange(messageBytes, 1, 7);
- byte[] raceID = Arrays.copyOfRange(messageBytes, 7, 11);
- byte[] windDirection = Arrays.copyOfRange(messageBytes, 11, 13);
- byte[] windSpeed = Arrays.copyOfRange(messageBytes, 13, 15);
- byte[] bestUpwindAngle = Arrays.copyOfRange(messageBytes, 15, 17);
- byte[] bestDownwindAngle = Arrays.copyOfRange(messageBytes, 17, 19);
- byte[] flags = Arrays.copyOfRange(messageBytes, 19, 20);
-
- CourseWind message = new CourseWind(windId[0], bytesToLong(time),
- bytesToInt(raceID), bytesToInt(windDirection),
- bytesToInt(windSpeed), bytesToInt(bestUpwindAngle),
- bytesToInt(bestDownwindAngle), flags[0]);
-
- loopMessages.add(message);
- messageLoopIndex += 20;
- }
- }
- public ArrayList getLoopMessages() {
- return loopMessages;
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
+
+ /**
+ * The decoded message.
+ */
+ private CourseWind message;
+
+
+
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public CourseWindDecoder() {
}
- public byte getMessageVersionNumber() {
- return messageVersionNumber;
+
+ /**
+ * Decodes the contained message.
+ * @param encodedMessage The message to decode.
+ * @return The decoded message.
+ * @throws InvalidMessageException Thrown if the encoded message is invalid in some way, or cannot be decoded.
+ */
+ public CourseWind decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
+
+ try {
+
+ byte[] windId = Arrays.copyOfRange(encodedMessage, 0, 1);
+
+ byte[] timeBytes = Arrays.copyOfRange(encodedMessage, 1, 7);
+ long time = bytesToLong(timeBytes);
+
+ byte[] raceIDBytes = Arrays.copyOfRange(encodedMessage, 7, 11);
+ int raceIDInt = bytesToInt(raceIDBytes);
+
+ byte[] windDirectionBytes = Arrays.copyOfRange(encodedMessage, 11, 13);
+ int windDirectionInt = bytesToInt(windDirectionBytes);
+ Bearing windDirection = Bearing.fromDegrees(unpackHeading(windDirectionInt));
+
+ byte[] windSpeedBytes = Arrays.copyOfRange(encodedMessage, 13, 15);
+ int windSpeedInt = bytesToInt(windSpeedBytes);
+ double windSpeedKnots = unpackMMperSecToKnots(windSpeedInt);
+
+ byte[] bestUpwindAngleBytes = Arrays.copyOfRange(encodedMessage, 15, 17);
+ int bestUpwindAngleInt = bytesToInt(bestUpwindAngleBytes);
+ Bearing bestUpwindAngle = Bearing.fromDegrees(unpackHeading(bestUpwindAngleInt));
+
+ byte[] bestDownwindAngleBytes = Arrays.copyOfRange(encodedMessage, 17, 19);
+ int bestDownwindAngleInt = bytesToInt(bestDownwindAngleBytes);
+ Bearing bestDownwindAngle = Bearing.fromDegrees(unpackHeading(bestDownwindAngleInt));
+
+ byte[] flags = Arrays.copyOfRange(encodedMessage, 19, 20);
+
+
+ message = new CourseWind(
+ windId[0],
+ time,
+ raceIDInt,
+ windDirection,
+ windSpeedKnots,
+ bestUpwindAngle,
+ bestDownwindAngle,
+ flags[0]);
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode CourseWind message.", e);
+ }
}
- public byte getByteWindID() {
- return byteWindID;
+
+ /**
+ * Returns the decoded message.
+ * @return The decoded message.
+ */
+ public CourseWind getMessage() {
+ return message;
}
+
}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/CourseWindsDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/CourseWindsDecoder.java
new file mode 100644
index 00000000..d6b7b70e
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageDecoders/CourseWindsDecoder.java
@@ -0,0 +1,93 @@
+package network.MessageDecoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.CourseWind;
+import network.Messages.CourseWinds;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static network.Utils.ByteConverter.bytesToInt;
+import static network.Utils.ByteConverter.bytesToLong;
+
+
+/**
+ * Decodes {@link CourseWinds} messages.
+ */
+public class CourseWindsDecoder implements MessageDecoder {
+
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
+
+ /**
+ * The decoded message.
+ */
+ private CourseWinds message;
+
+
+
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public CourseWindsDecoder() {
+ }
+
+
+ @Override
+ public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
+
+ try {
+
+ //The header is three bytes.
+ byte messageVersionNumber = encodedMessage[0];
+ byte byteWindID = encodedMessage[1];
+ byte loopCount = encodedMessage[2];
+
+
+ //A CourseWind object is 20 bytes.
+ final int courseWindByteLength = 20;
+
+ List loopMessages = new ArrayList();
+
+ //The header is 3 bytes, so we need the remaining bytes.
+ byte[] loopMessagesBytes = Arrays.copyOfRange(encodedMessage, 3, courseWindByteLength * loopCount + 3);
+
+ for (int messageLoopIndex = 0; messageLoopIndex < (loopCount * courseWindByteLength); messageLoopIndex += courseWindByteLength) {
+
+ byte[] messageBytes = Arrays.copyOfRange(loopMessagesBytes, messageLoopIndex, messageLoopIndex + courseWindByteLength);
+
+ CourseWindDecoder courseWindDecoder = new CourseWindDecoder();
+ CourseWind courseWind = courseWindDecoder.decode(messageBytes);
+
+ loopMessages.add(courseWind);
+ }
+
+
+ message = new CourseWinds(
+ messageVersionNumber,
+ byteWindID,
+ loopMessages);
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode CourseWinds message.", e);
+ }
+ }
+
+
+ /**
+ * Returns the decoded message.
+ * @return The decoded message.
+ */
+ public CourseWinds getMessage() {
+ return message;
+ }
+
+}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/DecoderFactory.java b/racevisionGame/src/main/java/network/MessageDecoders/DecoderFactory.java
new file mode 100644
index 00000000..09ea4b95
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageDecoders/DecoderFactory.java
@@ -0,0 +1,70 @@
+package network.MessageDecoders;
+
+
+import network.Exceptions.InvalidMessageTypeException;
+import network.Messages.Enums.MessageType;
+
+/**
+ * Factory to create the appropriate decoder for a given message.
+ */
+public class DecoderFactory {
+
+
+ /**
+ * Private constructor. Currently doesn't need to be constructed.
+ */
+ private DecoderFactory(){
+
+ }
+
+
+ /**
+ * Creates the correct type of decoder for a given message type.
+ * @param type Type of message you want a decoder for.
+ * @return The decoder.
+ * @throws InvalidMessageTypeException If you pass in a {@link MessageType} that isn't recognised.
+ */
+ public static MessageDecoder create(MessageType type) throws InvalidMessageTypeException {
+
+
+ switch (type) {
+
+ case HEARTBEAT: return new HeartBeatDecoder();
+
+ case RACESTATUS: return new RaceStatusDecoder();
+
+ //case DISPLAYTEXTMESSAGE: return new DisplayTextMessageDecoder();//TODO
+
+ case XMLMESSAGE: return new XMLMessageDecoder();
+
+ case RACESTARTSTATUS: return new RaceStartStatusDecoder();
+
+ //case YACHTEVENTCODE: return new YachtEventCodeDecoder();//TODO
+
+ //case YACHTACTIONCODE: return new YachtActionCodeDecoder();//TODO
+
+ //case CHATTERTEXT: return new ChatterTextDecoder();//TODO
+
+ case BOATLOCATION: return new BoatLocationDecoder();
+
+ case MARKROUNDING: return new MarkRoundingDecoder();
+
+ case COURSEWIND: return new CourseWindsDecoder();
+
+ case AVGWIND: return new AverageWindDecoder();
+
+ case REQUEST_TO_JOIN: return new RequestToJoinDecoder();
+
+ case JOIN_ACCEPTANCE: return new JoinAcceptanceDecoder();
+
+ case BOATACTION: return new BoatActionDecoder();
+
+
+ default: throw new InvalidMessageTypeException("Unrecognised message type: " + type);
+ }
+
+
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/HeartBeatDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/HeartBeatDecoder.java
new file mode 100644
index 00000000..bc86244b
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageDecoders/HeartBeatDecoder.java
@@ -0,0 +1,57 @@
+package network.MessageDecoders;
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.Enums.BoatActionEnum;
+import network.Messages.HeartBeat;
+
+import static network.Utils.ByteConverter.bytesToLong;
+
+/**
+ * Decodes {@link network.Messages.HeartBeat} messages.
+ */
+public class HeartBeatDecoder implements MessageDecoder {
+
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
+
+ /**
+ * The decoded message.
+ */
+ private HeartBeat message;
+
+
+
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public HeartBeatDecoder() {
+ }
+
+ @Override
+ public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
+
+ try {
+
+ message = new HeartBeat(bytesToLong(encodedMessage));
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode HeartBeat message.", e);
+ }
+ }
+
+
+ /**
+ * Returns the decoded message.
+ * @return The decoded message.
+ */
+ public HeartBeat getMessage() {
+ return message;
+ }
+
+}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/JoinAcceptanceDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/JoinAcceptanceDecoder.java
new file mode 100644
index 00000000..5766b47f
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageDecoders/JoinAcceptanceDecoder.java
@@ -0,0 +1,74 @@
+package network.MessageDecoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.Enums.JoinAcceptanceEnum;
+import network.Messages.JoinAcceptance;
+import network.Utils.ByteConverter;
+
+import java.util.Arrays;
+
+/**
+ * Decoder for {@link JoinAcceptance} messages.
+ */
+public class JoinAcceptanceDecoder implements MessageDecoder {
+
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
+
+ /**
+ * The decoded message.
+ */
+ private JoinAcceptance message;
+
+
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public JoinAcceptanceDecoder() {
+ }
+
+
+ @Override
+ public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
+
+ try {
+
+ //SourceID is first four bytes.
+ byte[] sourceIdBytes = Arrays.copyOfRange(encodedMessage, 0, 4);
+
+ //Next byte is acceptance type.
+ byte[] acceptanceBytes = Arrays.copyOfRange(encodedMessage, 4, 5);
+
+
+ //SourceID is an int.
+ int sourceID = ByteConverter.bytesToInt(sourceIdBytes);
+
+ //Acceptance enum is a byte.
+ JoinAcceptanceEnum acceptanceType = JoinAcceptanceEnum.fromByte(acceptanceBytes[0]);
+
+
+ message = new JoinAcceptance(acceptanceType, sourceID);
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode JoinAcceptance message.", e);
+ }
+ }
+
+
+ /**
+ * Returns the decoded message.
+ * @return The decoded message.
+ */
+ public JoinAcceptance getMessage() {
+ return message;
+ }
+
+
+}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java
index 000e86ee..6db46f27 100644
--- a/racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java
+++ b/racevisionGame/src/main/java/network/MessageDecoders/MarkRoundingDecoder.java
@@ -1,52 +1,97 @@
package network.MessageDecoders;
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.Enums.MarkRoundingBoatStatusEnum;
+import network.Messages.Enums.MarkRoundingSideEnum;
+import network.Messages.Enums.MarkRoundingTypeEnum;
import network.Messages.MarkRounding;
import network.Utils.ByteConverter;
import java.util.Arrays;
/**
- * Created by hba56 on 23/04/17.
+ * Decoder for {@link MarkRounding} messages.
*/
-public class MarkRoundingDecoder {
- byte messageVersionNumber;
- byte[] byteTime;
- byte[] byteAck;
- byte[] byteRaceID;
- byte[] byteSourceID;
- byte byteBoatStatus;
- byte byteRoundingSide;
- byte byteMarkType;
- byte byteMarkID;
-
- MarkRounding markRounding;
-
- public MarkRoundingDecoder(byte[] encodedMarkRounding) {
- messageVersionNumber = encodedMarkRounding[0];
- byteTime = Arrays.copyOfRange(encodedMarkRounding, 1, 7);
- byteAck = Arrays.copyOfRange(encodedMarkRounding, 7, 9);
- byteRaceID = Arrays.copyOfRange(encodedMarkRounding, 9, 13);
- byteSourceID = Arrays.copyOfRange(encodedMarkRounding, 13, 17);
- byteBoatStatus = encodedMarkRounding[17];
- byteRoundingSide = encodedMarkRounding[18];
- byteMarkType = encodedMarkRounding[19];
- byteMarkID = encodedMarkRounding[20];
-
- int intMsgVer = ByteConverter.bytesToInt(messageVersionNumber);
- long lngTime = ByteConverter.bytesToLong(byteTime);
- int intAck = ByteConverter.bytesToInt(byteAck);
- int intRaceID = ByteConverter.bytesToInt(byteRaceID);
- int intSourceID = ByteConverter.bytesToInt(byteSourceID);
- int intBoatState = ByteConverter.bytesToInt(byteBoatStatus);
- int intRoundingSide = ByteConverter.bytesToInt(byteRoundingSide);
- int intMarkType = ByteConverter.bytesToInt(byteMarkType);
- int intMarkID = ByteConverter.bytesToInt(byteMarkID);
-
- markRounding = new MarkRounding(intMsgVer, lngTime, intAck, intRaceID, intSourceID, intBoatState, intRoundingSide, intMarkType, intMarkID);
+public class MarkRoundingDecoder implements MessageDecoder {
+
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
+
+ /**
+ * The decoded message.
+ */
+ private MarkRounding message;
+
+
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public MarkRoundingDecoder() {
}
- public MarkRounding getMarkRounding() {
- return markRounding;
+ @Override
+ public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
+
+ try {
+
+ byte messageVersionNumber = encodedMessage[0];
+
+ byte[] byteTime = Arrays.copyOfRange(encodedMessage, 1, 7);
+ long time = ByteConverter.bytesToLong(byteTime);
+
+ byte[] byteAck = Arrays.copyOfRange(encodedMessage, 7, 9);
+ int ackNumber = ByteConverter.bytesToInt(byteAck);
+
+ byte[] byteRaceID = Arrays.copyOfRange(encodedMessage, 9, 13);
+ int raceID = ByteConverter.bytesToInt(byteRaceID);
+
+ byte[] byteSourceID = Arrays.copyOfRange(encodedMessage, 13, 17);
+ int sourceID = ByteConverter.bytesToInt(byteSourceID);
+
+ byte byteBoatStatus = encodedMessage[17];
+ MarkRoundingBoatStatusEnum boatStatus = MarkRoundingBoatStatusEnum.fromByte(byteBoatStatus);
+
+ byte byteRoundingSide = encodedMessage[18];
+ MarkRoundingSideEnum roundingSide = MarkRoundingSideEnum.fromByte(byteRoundingSide);
+
+ byte byteMarkType = encodedMessage[19];
+ MarkRoundingTypeEnum markType = MarkRoundingTypeEnum.fromByte(byteMarkType);
+
+ byte byteMarkID = encodedMessage[20];
+
+
+ message = new MarkRounding(
+ messageVersionNumber,
+ time,
+ ackNumber,
+ raceID,
+ sourceID,
+ boatStatus,
+ roundingSide,
+ markType,
+ byteMarkID);
+
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode AverageWind message.", e);
+ }
+ }
+
+ /**
+ * Returns the decoded message.
+ *
+ * @return The decoded message.
+ */
+ public MarkRounding getMessage() {
+ return message;
}
+
}
+
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/MessageDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/MessageDecoder.java
new file mode 100644
index 00000000..c20c653f
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageDecoders/MessageDecoder.java
@@ -0,0 +1,23 @@
+package network.MessageDecoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+
+
+/**
+ * This is the interface that all message decoders must implement.
+ * It allows for {@link #decode(byte[])}ing messages.
+ */
+public interface MessageDecoder {
+
+
+ /**
+ * Decodes a given message.
+ * @param encodedMessage The message to decode.
+ * @return The decoded message.
+ * @throws InvalidMessageException Thrown if the encoded message is invalid in some way, or cannot be decoded.
+ */
+ public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException;
+
+}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java
index 236c5d27..2776ea6f 100644
--- a/racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java
+++ b/racevisionGame/src/main/java/network/MessageDecoders/RaceStartStatusDecoder.java
@@ -1,66 +1,86 @@
package network.MessageDecoders;
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.Enums.RaceStartTypeEnum;
+import network.Messages.RaceStartStatus;
+
import java.util.Arrays;
import static network.Utils.ByteConverter.*;
/**
- * Created by hba56 on 21/04/17.
+ * Decodes {@link RaceStartStatus} messages.
*/
-public class RaceStartStatusDecoder {
- private byte messageVersion;
- private byte[] timestamp;
- private byte[] ackNumber;
- private byte[] raceStartTime;
- private byte[] raceIdentifier;
- private byte notificationType;
-
- private long time;
- private short ack;
- private long startTime;
- private int raceID;
- private char notification;
-
-
- public RaceStartStatusDecoder(byte[] encodedRaceStartStatus) {
- messageVersion = encodedRaceStartStatus[0];
- timestamp = Arrays.copyOfRange(encodedRaceStartStatus, 1, 7);
- ackNumber = Arrays.copyOfRange(encodedRaceStartStatus, 7, 9);
- raceStartTime = Arrays.copyOfRange(encodedRaceStartStatus, 9, 15);
- raceIdentifier = Arrays.copyOfRange(encodedRaceStartStatus, 15, 19);
- notificationType = encodedRaceStartStatus[19];
-
- time = bytesToLong(timestamp);
- ack = bytesToShort(ackNumber);
- startTime = bytesToLong(raceStartTime);
- raceID = bytesToInt(raceIdentifier);
- notification = bytesToChar(notificationType);
- }
+public class RaceStartStatusDecoder implements MessageDecoder {
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
- public byte getMessageVersion() {
- return messageVersion;
- }
+ /**
+ * The decoded message.
+ */
+ private RaceStartStatus message;
- public long getTime() {
- return time;
- }
- public short getAck() {
- return ack;
- }
- public long getStartTime() {
- return startTime;
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public RaceStartStatusDecoder() {
}
- public int getRaceID() {
- return raceID;
+
+ @Override
+ public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
+
+ try {
+
+ byte messageVersion = encodedMessage[0];
+
+ byte[] timestamp = Arrays.copyOfRange(encodedMessage, 1, 7);
+ long time = bytesToLong(timestamp);
+
+ byte[] ackNumber = Arrays.copyOfRange(encodedMessage, 7, 9);
+ short ack = bytesToShort(ackNumber);
+
+ byte[] raceStartTime = Arrays.copyOfRange(encodedMessage, 9, 15);
+ long startTime = bytesToLong(raceStartTime);
+
+ byte[] raceIdentifier = Arrays.copyOfRange(encodedMessage, 15, 19);
+ int raceID = bytesToInt(raceIdentifier);
+
+ byte notificationType = encodedMessage[19];
+
+
+ message = new RaceStartStatus(
+ messageVersion,
+ time,
+ ack,
+ startTime,
+ raceID,
+ RaceStartTypeEnum.fromByte(notificationType)
+ );
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode RaceStartStatus message.", e);
+ }
+
}
- public char getNotification() {
- return notification;
+
+ /**
+ * Returns the decoded message.
+ * @return The decoded message.
+ */
+ public RaceStartStatus getMessage() {
+ return message;
}
}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java
index e4d147df..7e26e105 100644
--- a/racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java
+++ b/racevisionGame/src/main/java/network/MessageDecoders/RaceStatusDecoder.java
@@ -1,10 +1,18 @@
package network.MessageDecoders;
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
import network.Messages.BoatStatus;
+import network.Messages.Enums.RaceStatusEnum;
+import network.Messages.Enums.RaceTypeEnum;
+import network.Messages.RaceStatus;
+import network.Utils.AC35UnitConverter;
+import shared.model.Bearing;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
import static network.Utils.ByteConverter.bytesToInt;
import static network.Utils.ByteConverter.bytesToLong;
@@ -12,110 +20,108 @@ import static network.Utils.ByteConverter.bytesToShort;
/**
- * Created by hba56 on 21/04/17.
+ * Decodes {@link RaceStatus} messages.
*/
-public class RaceStatusDecoder {
- private byte versionNum;
- private byte[] timeBytes;
- private byte[] raceID;
- private byte raceStatus;
- private byte[] expectedStart;
- private byte[] raceWind;
- private byte[] windSpeed;
- private byte numBoats;
- private byte bytesRaceType;
- private byte[] boatsBytes;
-
- private long time;
- private int race;
- private byte raceState;
- private long startTime;
- private int raceWindDir;
- private short raceWindSpeed;
- private int numberOfBoats;
- private int raceType;
- private ArrayList boats = new ArrayList<>();
-
-
- public RaceStatusDecoder(byte[] encodedRaceStatus){
- versionNum = encodedRaceStatus[0];
- timeBytes = Arrays.copyOfRange(encodedRaceStatus, 1, 7);
- raceID = Arrays.copyOfRange(encodedRaceStatus, 7, 11);
- raceStatus = encodedRaceStatus[11];
- expectedStart = Arrays.copyOfRange(encodedRaceStatus, 12, 18);
- raceWind = Arrays.copyOfRange(encodedRaceStatus, 18, 20);
- windSpeed = Arrays.copyOfRange(encodedRaceStatus, 20, 22);
- numBoats = encodedRaceStatus[22];
- bytesRaceType = encodedRaceStatus[23];
- boatsBytes = Arrays.copyOfRange(encodedRaceStatus, 24, 25+20*this.numBoats);
-
- time = bytesToLong(timeBytes);
- race = bytesToInt(raceID);
- raceState = raceStatus;
- startTime = bytesToLong(expectedStart);
- raceWindDir = bytesToInt(raceWind);
- raceWindSpeed = bytesToShort(windSpeed);
- numberOfBoats = bytesToInt(numBoats);
-
- int boatLoopIndex = 0;
-
- for (int i=0; i < numberOfBoats; i++) {
- byte[] boatBytes = Arrays.copyOfRange(boatsBytes, boatLoopIndex, boatLoopIndex+20);
-
- byte[] sourceID = Arrays.copyOfRange(boatBytes, 0, 3);
- byte boatStatus = boatBytes[4];
- byte legNumber = boatBytes[5];
- byte numPenaltiesAwarded = boatBytes[6];
- byte numPenaltiesServed = boatBytes[7];
- byte[] estTimeAtNextMark = Arrays.copyOfRange(boatBytes, 8, 14);
- byte[] estTimeAtFinish = Arrays.copyOfRange(boatBytes, 14, 20);
-
- BoatStatus boat = new BoatStatus(bytesToInt(sourceID),boatStatus,
- legNumber, numPenaltiesAwarded, numPenaltiesServed,
- bytesToLong(estTimeAtNextMark), bytesToLong(estTimeAtFinish));
-
- boats.add(boat);
- boatLoopIndex += 20;
- }
- }
+public class RaceStatusDecoder implements MessageDecoder {
- public byte getVersionNum() {
- return versionNum;
- }
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
- public long getTime() {
- return time;
- }
+ /**
+ * The decoded message.
+ */
+ private RaceStatus message;
- public int getRace() {
- return race;
- }
- public byte getRaceState() {
- return raceState;
- }
- public long getStartTime() {
- return startTime;
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public RaceStatusDecoder() {
}
- public int getRaceWindDir() {
- return raceWindDir;
- }
- public short getRaceWindSpeed() {
- return raceWindSpeed;
- }
+ @Override
+ public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
- public int getNumberOfBoats() {
- return numberOfBoats;
- }
+ try {
+
+ byte[] versionNumBytes = Arrays.copyOfRange(encodedMessage, 0, 1);
+ byte versionNum = versionNumBytes[0];
+
+ byte[] timeBytes = Arrays.copyOfRange(encodedMessage, 1, 7);
+ long time = bytesToLong(timeBytes);
+
+ byte[] raceIDBytes = Arrays.copyOfRange(encodedMessage, 7, 11);
+ int raceID = bytesToInt(raceIDBytes);
+
+ byte[] raceStatusBytes = Arrays.copyOfRange(encodedMessage, 11, 12);
+ RaceStatusEnum raceStatus = RaceStatusEnum.fromByte(raceStatusBytes[0]);
+
+ byte[] expectedStartBytes = Arrays.copyOfRange(encodedMessage, 12, 18);
+ long expectedStart = bytesToLong(expectedStartBytes);
+
+ byte[] windDirectionBytes = Arrays.copyOfRange(encodedMessage, 18, 20);
+ int windDirectionInt = bytesToInt(windDirectionBytes);
+ Bearing windDirection = Bearing.fromDegrees(AC35UnitConverter.unpackHeading(windDirectionInt));
+
+ byte[] windSpeedBytes = Arrays.copyOfRange(encodedMessage, 20, 22);
+ int windSpeedInt = bytesToInt(windSpeedBytes);
+ double windSpeedKnots = AC35UnitConverter.unpackMMperSecToKnots(windSpeedInt);
- public int getRaceType() {
- return raceType;
+ byte[] numberOfBoatsBytes = Arrays.copyOfRange(encodedMessage, 22, 23);
+ int numberOfBoats = bytesToInt(numberOfBoatsBytes);
+
+ byte[] raceTypeBytes = Arrays.copyOfRange(encodedMessage, 23, 24);
+ RaceTypeEnum raceType = RaceTypeEnum.fromByte(raceTypeBytes[0]);
+
+ byte[] boatStatusesBytes = Arrays.copyOfRange(encodedMessage, 24, 25 + 20 * numberOfBoats);
+ List boatStatuses = new ArrayList<>();
+
+
+ //BoatStatus is 20 bytes.
+ int boatStatusByteLength = 20;
+
+
+ //Decode each BoatStatus.
+ for (int boatLoopIndex = 0; boatLoopIndex < (numberOfBoats * boatStatusByteLength); boatLoopIndex += boatStatusByteLength) {
+
+ byte[] boatStatusBytes = Arrays.copyOfRange(boatStatusesBytes, boatLoopIndex, boatLoopIndex + boatStatusByteLength);
+
+ BoatStatusDecoder boatStatusDecoder = new BoatStatusDecoder();
+
+ boatStatuses.add(boatStatusDecoder.decode(boatStatusBytes));
+ }
+
+
+ message = new RaceStatus(
+ versionNum,
+ time,
+ raceID,
+ raceStatus,
+ expectedStart,
+ windDirection,
+ windSpeedKnots,
+ raceType,
+ boatStatuses);
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode RaceStatus message.", e);
+ }
}
- public ArrayList getBoats() {
- return boats;
+
+
+ /**
+ * Returns the decoded message.
+ * @return The decoded message.
+ */
+ public RaceStatus getMessage() {
+ return message;
}
}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/RequestToJoinDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/RequestToJoinDecoder.java
new file mode 100644
index 00000000..59c881d1
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageDecoders/RequestToJoinDecoder.java
@@ -0,0 +1,67 @@
+package network.MessageDecoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.Enums.RequestToJoinEnum;
+import network.Messages.RequestToJoin;
+import network.Utils.ByteConverter;
+
+import java.util.Arrays;
+
+/**
+ * Decoder for {@link network.Messages.RequestToJoin} messages.
+ */
+public class RequestToJoinDecoder implements MessageDecoder{
+
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedRequest;
+
+ /**
+ * The decoded message.
+ */
+ private RequestToJoin message;
+
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public RequestToJoinDecoder() {
+ }
+
+
+ @Override
+ public AC35Data decode(byte[] encodedRequest) throws InvalidMessageException {
+ this.encodedRequest = encodedRequest;
+
+ try {
+
+ //Request type is first four bytes.
+ byte[] requestTypeBytes = Arrays.copyOfRange(encodedRequest, 0, 4);
+
+ //Request type is an integral type.
+ int requestTypeInt = ByteConverter.bytesToInt(requestTypeBytes);
+ RequestToJoinEnum requestType = RequestToJoinEnum.fromInt(requestTypeInt);
+
+
+ message = new RequestToJoin(requestType);
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode RequestToJoin message.", e);
+ }
+ }
+
+
+ /**
+ * Returns the decoded message.
+ * @return The decoded message.
+ */
+ public RequestToJoin getMessage() {
+ return message;
+ }
+
+
+}
diff --git a/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java b/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java
index 18ead92e..80a62048 100644
--- a/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java
+++ b/racevisionGame/src/main/java/network/MessageDecoders/XMLMessageDecoder.java
@@ -1,10 +1,10 @@
package network.MessageDecoders;
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
import network.Messages.Enums.XMLMessageType;
+import network.Messages.XMLMessage;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import static network.Utils.ByteConverter.bytesToLong;
@@ -12,73 +12,74 @@ import static network.Utils.ByteConverter.bytesToShort;
/**
- * Created by hba56 on 20/04/17.
+ * Decodes {@link network.Messages.XMLMessage} messages.
*/
-public class XMLMessageDecoder {
- private byte messageVersionNumber;
- private short ackNumber;
- private long timeStamp;
- private byte xmlMsgSubType;
- private short sequenceNumber;
- private short xmlMsgLength;
- private String xmlMessage;
-
- private byte[] bytes;
-
- public XMLMessageDecoder(byte[] bytes) {
- this.bytes = bytes;
- }
+public class XMLMessageDecoder implements MessageDecoder {
- public void decode(){
- byte[] ackNumberBytes = Arrays.copyOfRange(bytes, 1, 3);
- byte[] timeStampBytes = Arrays.copyOfRange(bytes, 3, 9);
- byte[] sequenceNumberBytes = Arrays.copyOfRange(bytes, 10, 12);
- byte[] xmlMsgLengthBytes = Arrays.copyOfRange(bytes, 12, 14);
- byte[] xmlMessagebytes = Arrays.copyOfRange(bytes, 14, bytes.length);
+ /**
+ * The encoded message.
+ */
+ private byte[] encodedMessage;
- this.xmlMsgSubType = bytes[9];
- this.messageVersionNumber = bytes[0];
- this.ackNumber = bytesToShort(ackNumberBytes);
+ /**
+ * The decoded message.
+ */
+ private XMLMessage message;
- this.timeStamp = bytesToLong(timeStampBytes);
- this.sequenceNumber = bytesToShort(sequenceNumberBytes);
- this.xmlMsgLength = bytesToShort(xmlMsgLengthBytes);
- this.xmlMessage = new String(xmlMessagebytes).trim();
- }
- public byte getMessageVersionNumber() {
- return messageVersionNumber;
+ /**
+ * Constructs a decoder to decode a given message.
+ */
+ public XMLMessageDecoder() {
}
- public short getAckNumber() {
- return ackNumber;
- }
- public long getTimeStamp() {
- return timeStamp;
- }
+ @Override
+ public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
+ this.encodedMessage = encodedMessage;
- public XMLMessageType getXmlMsgSubType() {
- return XMLMessageType.fromByte(xmlMsgSubType);
- }
+ try {
- public short getSequenceNumber() {
- return sequenceNumber;
- }
+ byte[] messageVersionNumberBytes = Arrays.copyOfRange(encodedMessage, 0, 1);
+ byte[] ackNumberBytes = Arrays.copyOfRange(encodedMessage, 1, 3);
+ byte[] timeStampBytes = Arrays.copyOfRange(encodedMessage, 3, 9);
+ byte[] xmlMsgSubTypeBytes = Arrays.copyOfRange(encodedMessage, 9, 10);
+ byte[] sequenceNumberBytes = Arrays.copyOfRange(encodedMessage, 10, 12);
+ byte[] xmlMsgLengthBytes = Arrays.copyOfRange(encodedMessage, 12, 14);
+ byte[] xmlMessagebytes = Arrays.copyOfRange(encodedMessage, 14, encodedMessage.length);
+
+
+ byte messageVersionNumber = messageVersionNumberBytes[0];
+ short ackNumber = bytesToShort(ackNumberBytes);
+ long timeStamp = bytesToLong(timeStampBytes);
+ XMLMessageType xmlMsgSubType = XMLMessageType.fromByte(xmlMsgSubTypeBytes[0]);
+ short sequenceNumber = bytesToShort(sequenceNumberBytes);
+ short xmlMsgLength = bytesToShort(xmlMsgLengthBytes);
+ String xmlMessage = new String(xmlMessagebytes);
- public short getXmlMsgLength() {
- return xmlMsgLength;
- }
+ message = new XMLMessage(
+ messageVersionNumber,
+ ackNumber,
+ timeStamp,
+ xmlMsgSubType,
+ sequenceNumber,
+ xmlMessage);
+
+ return message;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not decode XMLMessage message.", e);
+ }
+ }
/**
- * Returns the contents of the XML message (e.g., the contents of a race.xml file).
- * @return The contents of the XML message.
+ * Returns the decoded message.
+ * @return The decoded message.
*/
- public String getXmlMessageContents() {
- return xmlMessage;
+ public XMLMessage getMessage() {
+ return message;
}
-
}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/AverageWindEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/AverageWindEncoder.java
new file mode 100644
index 00000000..b8bce3df
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/AverageWindEncoder.java
@@ -0,0 +1,92 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.AverageWind;
+
+import java.nio.ByteBuffer;
+
+import static network.Utils.AC35UnitConverter.*;
+import static network.Utils.ByteConverter.intToBytes;
+import static network.Utils.ByteConverter.longToBytes;
+
+/**
+ * This encoder can encode a {@link AverageWind} message.
+ */
+public class AverageWindEncoder implements MessageEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public AverageWindEncoder() {
+ }
+
+
+ @Override
+ public byte[] encode(AC35Data message) throws InvalidMessageException {
+
+ try {
+
+ //Downcast.
+ AverageWind averageWind = (AverageWind) message;
+
+
+ byte messageVersionNumber = averageWind.getMessageVersionNumber();
+
+ long time = averageWind.getTime();
+ byte[] byteTime = longToBytes(time, 6);
+
+ long rawPeriod = averageWind.getRawPeriod();
+ int rawPeriodInt = packAverageWindPeriod(rawPeriod);
+ byte[] byteRawPeriod = intToBytes(rawPeriodInt, 2);
+
+ double rawSampleSpeed = averageWind.getRawSpeedKnots();
+ int rawSampleSpeedInt = packKnotsToMMperSec(rawSampleSpeed);
+ byte[] byteRawSpeed = intToBytes(rawSampleSpeedInt, 2);
+
+ long period2 = averageWind.getSampleTwoPeriod();
+ int period2Int = packAverageWindPeriod(period2);
+ byte[] bytePeriod2 = intToBytes(period2Int, 2);
+
+ double speed2 = averageWind.getSampleTwoSpeedKnots();
+ int speed2Int = packKnotsToMMperSec(speed2);
+ byte[] byteSpeed2 = intToBytes(speed2Int, 2);
+
+ long period3 = averageWind.getSampleThreePeriod();
+ int period3Int = packAverageWindPeriod(period3);
+ byte[] bytePeriod3 = intToBytes(period3Int, 2);
+
+ double speed3 = averageWind.getSampleThreeSpeedKnots();
+ int speed3Int = packKnotsToMMperSec(speed3);
+ byte[] byteSpeed3 = intToBytes(speed3Int, 2);
+
+ long period4 = averageWind.getSampleFourPeriod();
+ int period4Int = packAverageWindPeriod(period4);
+ byte[] bytePeriod4 = intToBytes(period4Int, 2);
+
+ double speed4 = averageWind.getSampleFourSpeedKnots();
+ int speed4Int = packKnotsToMMperSec(speed4);
+ byte[] byteSpeed4 = intToBytes(speed4Int, 2);
+
+
+ ByteBuffer result = ByteBuffer.allocate(23);
+ result.put(messageVersionNumber);
+ result.put(byteTime);
+ result.put(byteRawPeriod);
+ result.put(byteRawSpeed);
+ result.put(bytePeriod2);
+ result.put(byteSpeed2);
+ result.put(bytePeriod3);
+ result.put(byteSpeed3);
+ result.put(bytePeriod4);
+ result.put(byteSpeed4);
+ return result.array();
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode AverageWind message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/BoatActionEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/BoatActionEncoder.java
new file mode 100644
index 00000000..31e02386
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/BoatActionEncoder.java
@@ -0,0 +1,47 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.BoatAction;
+
+import java.nio.ByteBuffer;
+
+import static network.Utils.ByteConverter.intToBytes;
+
+/**
+ * This encoder can encode a {@link BoatAction} message.
+ */
+public class BoatActionEncoder implements MessageEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public BoatActionEncoder() {
+ }
+
+
+ @Override
+ public byte[] encode(AC35Data message) throws InvalidMessageException {
+
+ try {
+
+ //Downcast.
+ BoatAction boatAction = (BoatAction) message;
+
+ //Message is 1 byte.
+ ByteBuffer boatActionMessage = ByteBuffer.allocate(1);
+
+ boatActionMessage.put(intToBytes(boatAction.getBoatAction().getValue(), 1));
+
+ byte[] result = boatActionMessage.array();
+
+ return result;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode BoatAction message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/BoatLocationEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/BoatLocationEncoder.java
new file mode 100644
index 00000000..b982a592
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/BoatLocationEncoder.java
@@ -0,0 +1,91 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.BoatLocation;
+
+import java.nio.ByteBuffer;
+
+import static network.Utils.AC35UnitConverter.*;
+import static network.Utils.ByteConverter.intToBytes;
+import static network.Utils.ByteConverter.longToBytes;
+
+/**
+ * This encoder can encode a {@link BoatLocation} message.
+ */
+public class BoatLocationEncoder implements MessageEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public BoatLocationEncoder() {
+ }
+
+
+ @Override
+ public byte[] encode(AC35Data message) throws InvalidMessageException {
+
+ try {
+
+ //Downcast.
+ BoatLocation boatLocation = (BoatLocation) message;
+
+
+ int messageVersionNumber = 0b1;
+ byte[] messageVersionBytes = intToBytes(messageVersionNumber, 1);
+ byte[] time = longToBytes(boatLocation.getTime(), 6);
+ byte[] sourceID = intToBytes(boatLocation.getSourceID(), 4);
+ byte[] seqNum = longToBytes(boatLocation.getSequenceNumber(), 4);
+ byte[] deviceType = intToBytes(boatLocation.getDeviceType().getValue(), 1);
+ byte[] latitude = intToBytes(packGPS(boatLocation.getLatitude()), 4);
+ byte[] longitude = intToBytes(packGPS(boatLocation.getLongitude()), 4);
+ byte[] altitude = intToBytes(boatLocation.getAltitude(), 4);
+ byte[] heading = intToBytes(packHeading(boatLocation.getHeading().degrees()), 2);
+ byte[] pitch = intToBytes(boatLocation.getPitch(), 2);
+ byte[] roll = intToBytes(boatLocation.getRoll(), 2);
+ byte[] boatSpeed = intToBytes(packKnotsToMMperSec(boatLocation.getBoatSpeedKnots()), 2);
+ byte[] cog = intToBytes(packHeading(boatLocation.getBoatCOG().degrees()), 2);
+ byte[] sog = intToBytes(packKnotsToMMperSec(boatLocation.getBoatSOGKnots()), 2);
+ byte[] apparentWindSpeed = intToBytes(packKnotsToMMperSec(boatLocation.getApparentWindSpeedKnots()), 2);
+ byte[] apparentWindAngle = intToBytes(packTrueWindAngle(boatLocation.getApparentWindAngle().degrees()), 2);
+ byte[] trueWindSpeed = intToBytes(packKnotsToMMperSec(boatLocation.getTrueWindSpeedKnots()), 2);
+ byte[] trueWindDirection = intToBytes(packHeading(boatLocation.getTrueWindDirection().degrees()), 2);
+ byte[] trueWindAngle = intToBytes(packTrueWindAngle(boatLocation.getTrueWindAngle().degrees()), 2);
+ byte[] currentDrift = intToBytes(packKnotsToMMperSec(boatLocation.getCurrentDriftKnots()), 2);
+ byte[] currentSet = intToBytes(packHeading(boatLocation.getCurrentSet().degrees()), 2);
+ byte[] rudderAngle = intToBytes(packTrueWindAngle(boatLocation.getRudderAngle().degrees()), 2);
+
+ ByteBuffer result = ByteBuffer.allocate(56);
+ result.put(messageVersionBytes);
+ result.put(time);
+ result.put(sourceID);
+ result.put(seqNum);
+ result.put(deviceType);
+ result.put(latitude);
+ result.put(longitude);
+ result.put(altitude);
+ result.put(heading);
+ result.put(pitch);
+ result.put(roll);
+ result.put(boatSpeed);
+ result.put(cog);
+ result.put(sog);
+ result.put(apparentWindSpeed);
+ result.put(apparentWindAngle);
+ result.put(trueWindSpeed);
+ result.put(trueWindDirection);
+ result.put(trueWindAngle);
+ result.put(currentDrift);
+ result.put(currentSet);
+ result.put(rudderAngle);
+
+ return result.array();
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode BoatLocation message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/BoatStatusEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/BoatStatusEncoder.java
new file mode 100644
index 00000000..fe7ecb18
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/BoatStatusEncoder.java
@@ -0,0 +1,65 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.BoatStatus;
+
+import java.nio.ByteBuffer;
+
+import static network.Utils.ByteConverter.intToBytes;
+import static network.Utils.ByteConverter.longToBytes;
+
+/**
+ * This encoder can encode a {@link BoatStatus} message.
+ */
+public class BoatStatusEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public BoatStatusEncoder() {
+ }
+
+
+ /**
+ * Encodes a given BoatStatus message.
+ * @param message The message to encode.
+ * @return The encoded message.
+ * @throws InvalidMessageException Thrown if the message is invalid in some way, or cannot be encoded.
+ */
+ public byte[] encode(BoatStatus message) throws InvalidMessageException {
+
+ try {
+
+
+ //Downcast.
+ BoatStatus boatStatus = (BoatStatus) message;
+
+ //BoatStatus is 20 bytes.
+ ByteBuffer boatStatusBuffer = ByteBuffer.allocate(20);
+
+ byte[] sourceID = intToBytes(boatStatus.getSourceID());
+ byte[] boatStatusBytes = intToBytes(boatStatus.getBoatStatus().getValue(), 1);
+ byte[] legNum = intToBytes(boatStatus.getLegNumber(), 1);
+ byte[] numPenalties = intToBytes(boatStatus.getNumPenaltiesAwarded(), 1);
+ byte[] numPenaltiesServed = intToBytes(boatStatus.getNumPenaltiesServed(), 1);
+ byte[] estNextMarkTime = longToBytes(boatStatus.getEstTimeAtNextMark(), 6);
+ byte[] estFinishTime = longToBytes(boatStatus.getEstTimeAtFinish(), 6);
+
+ boatStatusBuffer.put(sourceID);
+ boatStatusBuffer.put(boatStatusBytes);
+ boatStatusBuffer.put(legNum);
+ boatStatusBuffer.put(numPenalties);
+ boatStatusBuffer.put(numPenaltiesServed);
+ boatStatusBuffer.put(estNextMarkTime);
+ boatStatusBuffer.put(estFinishTime);
+
+ return boatStatusBuffer.array();
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode BoatStatus message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/CourseWindEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/CourseWindEncoder.java
new file mode 100644
index 00000000..2f5f4897
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/CourseWindEncoder.java
@@ -0,0 +1,81 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.CourseWind;
+import shared.model.Bearing;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import static network.Utils.AC35UnitConverter.*;
+import static network.Utils.ByteConverter.*;
+import static network.Utils.ByteConverter.bytesToInt;
+
+/**
+ * This encoder can encode a {@link CourseWind} message.
+ */
+public class CourseWindEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public CourseWindEncoder() {
+ }
+
+
+ /**
+ * Encodes a given CourseWind message.
+ * @param message The message to encode.
+ * @return The encoded message.
+ * @throws InvalidMessageException Thrown if the message is invalid in some way, or cannot be encoded.
+ */
+ public byte[] encode(CourseWind message) throws InvalidMessageException {
+
+ try {
+
+ CourseWind courseWind = message;
+
+
+ //CourseWind is 20 bytes.
+ ByteBuffer courseWindBuffer = ByteBuffer.allocate(20);
+
+
+ byte[] windId = intToBytes(courseWind.getID(), 1);
+
+ byte[] timeBytes = longToBytes(courseWind.getTime(), 6);
+
+ byte[] raceIDBytes = intToBytes(courseWind.getRaceID(), 4);
+
+ int windDirectionInt = packHeading(courseWind.getWindDirection().degrees());
+ byte[] windDirectionBytes = intToBytes(windDirectionInt, 2);
+
+ int windSpeedInt = packKnotsToMMperSec(courseWind.getWindSpeedKnots());
+ byte[] windSpeedBytes = intToBytes(windSpeedInt, 2);
+
+ int bestUpwindAngleInt = packHeading(courseWind.getBestUpwindAngle().degrees());
+ byte[] bestUpwindAngleBytes = intToBytes(bestUpwindAngleInt, 2);
+
+ int bestDownwindAngleInt = packHeading(courseWind.getBestDownwindAngle().degrees());
+ byte[] bestDownwindAngleBytes = intToBytes(bestDownwindAngleInt, 2);
+
+ byte[] flags = intToBytes(courseWind.getFlags(), 1);
+
+ courseWindBuffer.put(windId);
+ courseWindBuffer.put(timeBytes);
+ courseWindBuffer.put(raceIDBytes);
+ courseWindBuffer.put(windDirectionBytes);
+ courseWindBuffer.put(windSpeedBytes);
+ courseWindBuffer.put(bestUpwindAngleBytes);
+ courseWindBuffer.put(bestDownwindAngleBytes);
+ courseWindBuffer.put(flags);
+
+ return courseWindBuffer.array();
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode CourseWind message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/CourseWindsEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/CourseWindsEncoder.java
new file mode 100644
index 00000000..86bd1197
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/CourseWindsEncoder.java
@@ -0,0 +1,64 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.BoatAction;
+import network.Messages.CourseWind;
+import network.Messages.CourseWinds;
+
+import java.nio.ByteBuffer;
+
+import static network.Utils.ByteConverter.intToBytes;
+
+/**
+ * This encoder can encode a {@link CourseWinds} message.
+ */
+public class CourseWindsEncoder implements MessageEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public CourseWindsEncoder() {
+ }
+
+
+ @Override
+ public byte[] encode(AC35Data message) throws InvalidMessageException {
+
+ try {
+
+ //Downcast.
+ CourseWinds courseWinds = (CourseWinds) message;
+
+
+ byte messageVersionNumber = CourseWinds.currentMessageVersionNumber;
+
+ byte byteWindID = courseWinds.getSelectedWindID();
+
+ byte[] loopcount = intToBytes(courseWinds.getCourseWinds().size(), 1);
+
+ ByteBuffer result = ByteBuffer.allocate(3 + 20 * courseWinds.getCourseWinds().size());
+
+ result.put(messageVersionNumber);
+ result.put(byteWindID);
+ result.put(loopcount);
+
+ //Encode each CourseWind.
+ for (CourseWind wind : courseWinds.getCourseWinds()) {
+
+ CourseWindEncoder courseWindEncoder = new CourseWindEncoder();
+ byte[] encodedCourseWind = courseWindEncoder.encode(wind);
+
+ result.put(encodedCourseWind);
+ }
+ return result.array();
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode CourseWinds message.", e);
+ }
+
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/EncoderFactory.java b/racevisionGame/src/main/java/network/MessageEncoders/EncoderFactory.java
new file mode 100644
index 00000000..b59150e4
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/EncoderFactory.java
@@ -0,0 +1,70 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageTypeException;
+import network.Messages.Enums.MessageType;
+
+/**
+ * Factory to create the appropriate encoder for a given message.
+ */
+public class EncoderFactory {
+
+
+ /**
+ * Private constructor. Currently doesn't need to be constructed.
+ */
+ private EncoderFactory(){
+
+ }
+
+
+ /**
+ * Creates the correct type of encoder for a given message type.
+ * @param type Type of message you want an encoder for.
+ * @return The encoder.
+ * @throws InvalidMessageTypeException If you pass in a {@link MessageType} that isn't recognised.
+ */
+ public static MessageEncoder create(MessageType type) throws InvalidMessageTypeException {
+
+
+ switch (type) {
+
+ case HEARTBEAT: return new HeartBeatEncoder();
+
+ case RACESTATUS: return new RaceStatusEncoder();
+
+ //case DISPLAYTEXTMESSAGE: return new DisplayTextMessageEncoder();//TODO
+
+ case XMLMESSAGE: return new XMLMessageEncoder();
+
+ case RACESTARTSTATUS: return new RaceStartStatusEncoder();
+
+ //case YACHTEVENTCODE: return new YachtEventCodeEncoder();//TODO
+
+ //case YACHTACTIONCODE: return new YachtActionCodeEncoder();//TODO
+
+ //case CHATTERTEXT: return new ChatterTextEncoder();//TODO
+
+ case BOATLOCATION: return new BoatLocationEncoder();
+
+ case MARKROUNDING: return new MarkRoundingEncoder();
+
+ case COURSEWIND: return new CourseWindsEncoder();
+
+ case AVGWIND: return new AverageWindEncoder();
+
+ case REQUEST_TO_JOIN: return new RequestToJoinEncoder();
+
+ case JOIN_ACCEPTANCE: return new JoinAcceptanceEncoder();
+
+ case BOATACTION: return new BoatActionEncoder();
+
+
+ default: throw new InvalidMessageTypeException("Unrecognised message type: " + type);
+ }
+
+
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/HeartBeatEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/HeartBeatEncoder.java
new file mode 100644
index 00000000..ecbefe41
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/HeartBeatEncoder.java
@@ -0,0 +1,46 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.HeartBeat;
+
+import java.nio.ByteBuffer;
+
+import static network.Utils.ByteConverter.longToBytes;
+
+/**
+ * This encoder can encode a {@link HeartBeat} message.
+ */
+public class HeartBeatEncoder implements MessageEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public HeartBeatEncoder() {
+ }
+
+
+ @Override
+ public byte[] encode(AC35Data message) throws InvalidMessageException {
+
+ try {
+
+ //Downcast.
+ HeartBeat heartbeat = (HeartBeat) message;
+
+ //Message is 4 bytes.
+ ByteBuffer heartBeat = ByteBuffer.allocate(4);
+ heartBeat.put(longToBytes(heartbeat.getSequenceNumber(), 4));
+
+ byte[] result = heartBeat.array();
+
+ return result;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode HeartBeat message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/JoinAcceptanceEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/JoinAcceptanceEncoder.java
new file mode 100644
index 00000000..8f236454
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/JoinAcceptanceEncoder.java
@@ -0,0 +1,51 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.JoinAcceptance;
+import network.Utils.ByteConverter;
+
+import java.nio.ByteBuffer;
+
+import static network.Utils.ByteConverter.intToBytes;
+
+/**
+ * This encoder can encode a {@link JoinAcceptance} message.
+ */
+public class JoinAcceptanceEncoder implements MessageEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public JoinAcceptanceEncoder() {
+ }
+
+
+ @Override
+ public byte[] encode(AC35Data message) throws InvalidMessageException {
+
+ try {
+
+ //Downcast.
+ JoinAcceptance joinAcceptance = (JoinAcceptance) message;
+
+ //Message is 5 bytes.
+ ByteBuffer joinAcceptanceBuffer = ByteBuffer.allocate(5);
+
+ //Source ID is first four bytes.
+ joinAcceptanceBuffer.put(intToBytes(joinAcceptance.getSourceID(), 4));
+ //Acceptance type is next byte.
+ joinAcceptanceBuffer.put(intToBytes(joinAcceptance.getAcceptanceType().getValue(), 1));
+
+ byte[] result = joinAcceptanceBuffer.array();
+
+ return result;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode JoinAcceptance message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/MarkRoundingEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/MarkRoundingEncoder.java
new file mode 100644
index 00000000..2c30b0cd
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/MarkRoundingEncoder.java
@@ -0,0 +1,64 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.MarkRounding;
+
+import java.nio.ByteBuffer;
+
+import static network.Utils.ByteConverter.intToBytes;
+import static network.Utils.ByteConverter.longToBytes;
+
+/**
+ * This encoder can encode a {@link MarkRounding} message.
+ */
+public class MarkRoundingEncoder implements MessageEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public MarkRoundingEncoder() {
+ }
+
+
+ @Override
+ public byte[] encode(AC35Data message) throws InvalidMessageException {
+
+ try {
+
+ //Downcast.
+ MarkRounding markRounding = (MarkRounding) message;
+
+ byte messageVersionNumber = markRounding.getMessageVersionNumber();
+ byte[] byteTime = longToBytes(markRounding.getTime(), 6);
+ byte[] byteAck = intToBytes(markRounding.getAckNum(), 2);
+ byte[] byteRaceID = intToBytes(markRounding.getRaceID(), 4);
+ byte[] byteSourceID = intToBytes(markRounding.getSourceID(), 4);
+ byte[] byteBoatStatus = intToBytes(markRounding.getBoatStatus().getValue(), 1);
+ byte[] byteRoundingSide = intToBytes(markRounding.getRoundingSide().getValue(), 1);
+ byte[] byteMarkType = intToBytes(markRounding.getMarkType().getValue(), 1);
+ byte[] byteMarkID = intToBytes(markRounding.getMarkID(), 1);
+
+
+ ByteBuffer result = ByteBuffer.allocate(21);
+
+ result.put(messageVersionNumber);
+ result.put(byteTime);
+ result.put(byteAck);
+ result.put(byteRaceID);
+ result.put(byteSourceID);
+ result.put(byteBoatStatus);
+ result.put(byteRoundingSide);
+ result.put(byteMarkType);
+ result.put(byteMarkID);
+
+ return result.array();
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode MarkRounding message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/MessageEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/MessageEncoder.java
new file mode 100644
index 00000000..f91bb639
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/MessageEncoder.java
@@ -0,0 +1,23 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+
+
+/**
+ * This is the interface that all message encoders must implement.
+ * It allows for {@link #encode(AC35Data)}ing messages.
+ */
+public interface MessageEncoder {
+
+
+ /**
+ * Encodes a given message.
+ * @param message The message to encode.
+ * @return Message in byte encoded form.
+ * @throws InvalidMessageException Thrown if the message is invalid in some way, or cannot be encoded.
+ */
+ public byte[] encode(AC35Data message) throws InvalidMessageException;
+
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/RaceStartStatusEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/RaceStartStatusEncoder.java
new file mode 100644
index 00000000..ad9a65f0
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/RaceStartStatusEncoder.java
@@ -0,0 +1,58 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.RaceStartStatus;
+import network.Utils.ByteConverter;
+
+import java.nio.ByteBuffer;
+
+import static network.Utils.ByteConverter.intToBytes;
+import static network.Utils.ByteConverter.longToBytes;
+
+/**
+ * This encoder can encode a {@link RaceStartStatus} message.
+ */
+public class RaceStartStatusEncoder implements MessageEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public RaceStartStatusEncoder() {
+ }
+
+
+ @Override
+ public byte[] encode(AC35Data message) throws InvalidMessageException {
+
+ try {
+
+ //Downcast.
+ RaceStartStatus raceStartStatus = (RaceStartStatus) message;
+
+
+ byte messageVersion = raceStartStatus.getMessageVersionNumber();
+ byte[] timestamp = longToBytes(raceStartStatus.getTimestamp(), 6);
+ byte[] ackNumber = intToBytes(raceStartStatus.getAckNum(), 2);
+ byte[] raceStartTime = longToBytes(raceStartStatus.getRaceStartTime(), 6);
+ byte[] raceIdentifier = intToBytes(raceStartStatus.getRaceID());
+ byte[] notificationType = intToBytes(raceStartStatus.getNotificationType().getValue(), 1);
+
+ ByteBuffer result = ByteBuffer.allocate(20);
+ result.put(messageVersion);
+ result.put(timestamp);
+ result.put(ackNumber);
+ result.put(raceStartTime);
+ result.put(raceIdentifier);
+ result.put(notificationType);
+
+ return result.array();
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode RaceStartStatus message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/RaceStatusEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/RaceStatusEncoder.java
new file mode 100644
index 00000000..fd87cd0c
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/RaceStatusEncoder.java
@@ -0,0 +1,102 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.BoatStatus;
+import network.Messages.RaceStatus;
+import network.Utils.AC35UnitConverter;
+import shared.model.Bearing;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import static network.Utils.ByteConverter.bytesToInt;
+import static network.Utils.ByteConverter.intToBytes;
+import static network.Utils.ByteConverter.longToBytes;
+
+/**
+ * This encoder can encode a {@link RaceStatus} message.
+ */
+public class RaceStatusEncoder implements MessageEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public RaceStatusEncoder() {
+ }
+
+
+ @Override
+ public byte[] encode(AC35Data message) throws InvalidMessageException {
+
+ try {
+
+ //Downcast.
+ RaceStatus raceStatus = (RaceStatus) message;
+
+
+ List boatStatuses = raceStatus.getBoatStatuses();
+
+ //24 byte header, plus 20 bytes per boat status.
+ ByteBuffer raceStatusMessage = ByteBuffer.allocate(24 + 20 * boatStatuses.size());
+
+ //Version Number 1 bytes. this changes with the pdf. (2)
+ byte versionNum = 0b10;
+
+ //time (6 bytes)
+ byte[] timeBytes = longToBytes(raceStatus.getCurrentTime(), 6);
+
+ //race identifier in case multiple races are going at once.
+ byte[] raceID = intToBytes(raceStatus.getRaceID());
+
+ //race status 0 - 10
+ byte[] raceStatusByte = intToBytes(raceStatus.getRaceStatus().getValue(), 1);
+
+ //number of milliseconds from Jan 1, 1970 for when the data is valid
+ byte[] expectedStart = longToBytes(raceStatus.getExpectedStartTime(), 6);
+
+ //North = 0x0000 East = 0x4000 South = 0x8000.
+ int windDirectionInt = AC35UnitConverter.packHeading(raceStatus.getWindDirection().degrees());
+ byte[] raceWind = intToBytes(windDirectionInt, 2);
+
+ //mm/sec
+ int windSpeedInt = AC35UnitConverter.packKnotsToMMperSec(raceStatus.getWindSpeed());
+ byte[] windSpeed = intToBytes(windSpeedInt, 2);
+
+
+ byte[] numBoats = intToBytes(boatStatuses.size(), 1);
+
+ //1 match race, 2 fleet race
+ byte[] bytesRaceType = intToBytes(raceStatus.getRaceType().getValue(), 1);
+
+
+ raceStatusMessage.put(versionNum);
+ raceStatusMessage.put(timeBytes);
+ raceStatusMessage.put(raceID);
+ raceStatusMessage.put(raceStatusByte);
+ raceStatusMessage.put(expectedStart);
+ raceStatusMessage.put(raceWind);
+ raceStatusMessage.put(windSpeed);
+ raceStatusMessage.put(numBoats);
+ raceStatusMessage.put(bytesRaceType);
+
+ //Encode each BoatStatus.
+ for (BoatStatus boatStatus : boatStatuses) {
+
+ BoatStatusEncoder boatStatusEncoder = new BoatStatusEncoder();
+
+ byte[] boatStatusEncoded = boatStatusEncoder.encode(boatStatus);
+
+ raceStatusMessage.put(boatStatusEncoded);
+ }
+
+ return raceStatusMessage.array();
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode RaceStatus message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java
index 4c57cf0c..303b30db 100644
--- a/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java
+++ b/racevisionGame/src/main/java/network/MessageEncoders/RaceVisionByteEncoder.java
@@ -1,7 +1,11 @@
package network.MessageEncoders;
+import network.BinaryMessageEncoder;
+import network.Exceptions.InvalidMessageException;
+import network.Exceptions.InvalidMessageTypeException;
import network.Messages.*;
+import network.Messages.Enums.MessageType;
import static network.Utils.ByteConverter.*;
@@ -17,72 +21,8 @@ import java.util.List;
*/
public class RaceVisionByteEncoder {
- /**
- * Serializes a heartbeat message.
- * @param heartbeat Heartbeat message.
- * @return Serialized message.
- */
- public static byte[] heartBeat(Heartbeat heartbeat) {
-
- ByteBuffer heartBeat = ByteBuffer.allocate(4);
- heartBeat.put(longToBytes(heartbeat.getSequenceNumber(), 4));
- byte[] result = heartBeat.array();
- return result;
- }
-
- /**
- * Serializes a RaceStatus message.
- * @param raceStatus Message to serialize.
- * @return Serialized (byte array) message, ready to be written to a socket.
- */
- public static byte[] raceStatus(RaceStatus raceStatus){
-
- List boatStatuses = raceStatus.getBoatStatuses();
-
- ByteBuffer raceStatusMessage = ByteBuffer.allocate(24 + 20* boatStatuses.size());
- //Version Number 1 bytes
- byte versionNum = 0b10; //this changes with the pdf. (2)
- byte[] timeBytes = longToBytes(raceStatus.getCurrentTime(), 6);//time (6 bytes)
- byte[] raceID = ByteBuffer.allocate(4).put(intToBytes(raceStatus.getRaceID())).array();//race identifier incase multiple races are going at once.
- byte[] raceStatusByte = intToBytes(raceStatus.getRaceStatus(), 1);//race status 0 - 10
- byte[] expectedStart = longToBytes(raceStatus.getExpectedStartTime(), 6);//number of milliseconds from Jan 1, 1970 for when the data is valid
- byte[] raceWind = ByteBuffer.allocate(2).put(intToBytes(raceStatus.getWindDirection(), 2)).array();//North = 0x0000 East = 0x4000 South = 0x8000.
- byte[] windSpeed = ByteBuffer.allocate(2).put(intToBytes(raceStatus.getWindSpeed(), 2)).array();//mm/sec
- byte[] numBoats = intToBytes(boatStatuses.size(), 1);
- byte[] bytesRaceType = intToBytes(raceStatus.getRaceType(), 1);//1 match race, 2 fleet race
-
- raceStatusMessage.put(versionNum);
- raceStatusMessage.put(timeBytes);
- raceStatusMessage.put(raceID);
- raceStatusMessage.put(raceStatusByte);
- raceStatusMessage.put(expectedStart);
- raceStatusMessage.put(raceWind);
- raceStatusMessage.put(windSpeed);
- raceStatusMessage.put(numBoats);
- raceStatusMessage.put(bytesRaceType);
-
- for (int i = 0; i < boatStatuses.size(); i++){
- byte[] sourceID = intToBytes(boatStatuses.get(i).getSourceID());
- byte[] boatStatus = intToBytes(boatStatuses.get(i).getBoatStatus(), 1);
- byte[] legNum = intToBytes(boatStatuses.get(i).getLegNumber(), 1);
- byte[] numPenalties = intToBytes(boatStatuses.get(i).getNumPenaltiesAwarded(), 1);
- byte[] numPenaltiesServed = intToBytes(boatStatuses.get(i).getNumPenaltiesServed(), 1);
- byte[] estNextMarkTime = longToBytes(boatStatuses.get(i).getEstTimeAtNextMark(), 6);
- byte[] estFinishTime = longToBytes( boatStatuses.get(i).getEstTimeAtFinish(), 6);
-
- raceStatusMessage.put(sourceID);
- raceStatusMessage.put(boatStatus);
- raceStatusMessage.put(legNum);
- raceStatusMessage.put(numPenalties);
- raceStatusMessage.put(numPenaltiesServed);
- raceStatusMessage.put(estNextMarkTime);
- raceStatusMessage.put(estFinishTime);
- }
-
- return raceStatusMessage.array();
- }
public static byte[] displayTextMessage(RaceMessage[] message){
//ByteBuffer result = ByteBuffer.allocate(4 + numLines * 32);
@@ -125,25 +65,6 @@ public class RaceVisionByteEncoder {
return result.array();
}
- public static byte[] raceStartStatus(long time, short ack, long startTime, int raceID, char notification){
- int messageVersion = 0b1;
- byte[] timestamp = longToBytes(time, 6);
- byte[] ackNumber = intToBytes(ack, 2);
- byte[] raceStartTime = longToBytes(startTime, 6);
- int raceIdentifier = raceID;
- byte[] notificationType = intToBytes(notification, 1);
-
- ByteBuffer result = ByteBuffer.allocate(20);
- result.put(intToBytes(messageVersion, 1));
- result.put(timestamp);
- result.put(ackNumber);
- result.put(raceStartTime);
- result.put(intToBytes(raceIdentifier));
- result.put(notificationType);
-
- return result.array();
- }
-
public static byte[] yachtEventCode(long time, short acknowledgeNumber, int raceID, int destSourceID, int incidentID,
int eventID){
int messageVersion = 0b10;
@@ -181,169 +102,54 @@ public class RaceVisionByteEncoder {
}
- /**
- * Serializes an xml message into a byte array.
- * @param xmlMessage The message to serialize.
- * @return byte array contaning serialized message.
- */
- public static byte[] xmlMessage(XMLMessage xmlMessage) {
-
-
- byte[] messageBytes = xmlMessage.getXmlMessage().getBytes(StandardCharsets.UTF_8);
- ByteBuffer tempOutputByteBuffer = ByteBuffer.allocate(14 + messageBytes.length);
- //ackNumber converted to bytes
- byte[] ackNumberBytes = intToBytes(xmlMessage.getAckNumber(), 2);
+ /**
+ * Encodes a given message, to be placed inside a binary message (see {@link BinaryMessageEncoder}).
+ * @param message Message to encode.
+ * @return Encoded message.
+ * @throws InvalidMessageException If the message cannot be encoded.
+ */
+ public static byte[] encode(AC35Data message) throws InvalidMessageException {
- //Timestamp converted to bytes.
- byte[] timestampBytes = longToBytes(xmlMessage.getTimeStamp(), 6);
+ MessageEncoder encoder = null;
+ try {
+ encoder = EncoderFactory.create(message.getType());
- //sequenceNumber converted to bytes
- byte[] sequenceNumberBytes = intToBytes(xmlMessage.getSequenceNumber(), 2);
+ } catch (InvalidMessageTypeException e) {
+ throw new InvalidMessageException("Could not create encoder for MessageType: " + message.getType(), e);
- //xmlMsgLength converted to bytes
- byte[] xmlMsgLengthBytes = intToBytes(xmlMessage.getXmlMsgLength(), 2);
+ }
+ byte[] encodedMessage = encoder.encode(message);
- tempOutputByteBuffer.put(xmlMessage.getVersionNumber());
- tempOutputByteBuffer.put(ackNumberBytes);
- tempOutputByteBuffer.put(timestampBytes);
- tempOutputByteBuffer.put(xmlMessage.getXmlMsgSubType().getValue());
- tempOutputByteBuffer.put(sequenceNumberBytes);
- tempOutputByteBuffer.put(xmlMsgLengthBytes);
- tempOutputByteBuffer.put(messageBytes);
+ return encodedMessage;
+ }
- return tempOutputByteBuffer.array();
- }
+ /**
+ * Encodes a given messages, using a given ackNumber, and returns a binary message ready to be sent over-the-wire.
+ * @param message The message to send.
+ * @param ackNumber The ackNumber of the message.
+ * @return A binary message ready to be transmitted.
+ * @throws InvalidMessageException Thrown if the message cannot be encoded.
+ */
+ public static byte[] encodeBinaryMessage(AC35Data message, int ackNumber) throws InvalidMessageException {
- public static byte[] boatLocation(BoatLocation boatLocation){
- int messageVersionNumber = 0b1;
- byte[] time = longToBytes(boatLocation.getTime(), 6);
- byte[] sourceID = intToBytes(boatLocation.getSourceID(), 4);
- byte[] seqNum = longToBytes(boatLocation.getSequenceNumber(), 4);
- byte[] deviceType = intToBytes(boatLocation.getDeviceType(), 1);
- byte[] latitude = intToBytes(boatLocation.getLatitude(), 4);
- byte[] longitude = intToBytes(boatLocation.getLongitude(), 4);
- byte[] altitude = intToBytes(boatLocation.getAltitude(), 4);
- byte[] heading = intToBytes(boatLocation.getHeading(), 2);
- byte[] pitch = intToBytes(boatLocation.getPitch(), 2);
- byte[] roll = intToBytes(boatLocation.getRoll(), 2);
- byte[] boatSpeed = intToBytes(boatLocation.getBoatSpeed(), 2);
- byte[] cog = intToBytes(boatLocation.getBoatCOG(), 2);
- byte[] sog = intToBytes(boatLocation.getBoatSOG(), 2);
- byte[] apparentWindSpeed = intToBytes(boatLocation.getApparentWindSpeed(), 2);
- byte[] apparentWindAngle = intToBytes(boatLocation.getApparentWindAngle(), 2);
- byte[] trueWindSpeed = intToBytes(boatLocation.getTrueWindSpeed(), 2);
- byte[] trueWindDirection = intToBytes(boatLocation.getTrueWindDirection(), 2);
- byte[] trueWindAngle = intToBytes(boatLocation.getTrueWindAngle(), 2);
- byte[] currentDrift = intToBytes(boatLocation.getCurrentDrift(), 2);
- byte[] currentSet = intToBytes(boatLocation.getCurrentSet(), 2);
- byte[] rudderAngle = intToBytes(boatLocation.getRudderAngle(), 2);
-
- ByteBuffer result = ByteBuffer.allocate(56);
- result.put(intToBytes(messageVersionNumber, 1));
- result.put(time);
- result.put(sourceID);
- result.put(seqNum);
- result.put(deviceType);
- result.put(latitude);
- result.put(longitude);
- result.put(altitude);
- result.put(heading);
- result.put(pitch);
- result.put(roll);
- result.put(boatSpeed);
- result.put(cog);
- result.put(sog);
- result.put(apparentWindSpeed);
- result.put(apparentWindAngle);
- result.put(trueWindSpeed);
- result.put(trueWindDirection);
- result.put(trueWindAngle);
- result.put(currentDrift);
- result.put(currentSet);
- result.put(rudderAngle);
- return result.array();
- }
+ //Encodes the message.
+ byte[] encodedMessage = RaceVisionByteEncoder.encode(message);
- public static byte[] markRounding(int time, int ackNumber, int raceID, int sourceID, int boatStatus, int roundingSide, int markType, int markID){
- int messageVersionNumber = 0b1;
- byte[] byteTime = longToBytes(time, 6);
- byte[] byteAck = intToBytes(ackNumber, 2);
- byte[] byteRaceID = intToBytes(raceID, 4);
- byte[] byteSourceID = intToBytes(sourceID, 4);
- byte[] byteBoatStatus = intToBytes(boatStatus, 1);
- byte[] byteRoundingSide = intToBytes(roundingSide, 1);
- byte[] byteMarkType = intToBytes(markType, 1);
- byte[] byteMarkID = intToBytes(markID, 1);
-
- ByteBuffer result = ByteBuffer.allocate(21);
- result.put(intToBytes(messageVersionNumber, 1));
- result.put(byteTime);
- result.put(byteAck);
- result.put(byteRaceID);
- result.put(byteSourceID);
- result.put(byteBoatStatus);
- result.put(byteRoundingSide);
- result.put(byteMarkType);
- result.put(byteMarkID);
- return result.array();
- }
+ //Encodes the full message with header.
+ BinaryMessageEncoder binaryMessageEncoder = new BinaryMessageEncoder(
+ message.getType(),
+ System.currentTimeMillis(),
+ ackNumber,
+ (short) encodedMessage.length,
+ encodedMessage );
- public static byte[] courseWind(byte windID, ArrayList courseWinds){
- int messageVersionNumber = 0b1;
- byte byteWindID = windID;
- byte[] loopcount = intToBytes(courseWinds.size(), 1);
- ByteBuffer result = ByteBuffer.allocate(3 + 20 * courseWinds.size());
- result.put(intToBytes(messageVersionNumber, 1));
- result.put(byteWindID);
- result.put(loopcount);
- for (CourseWind wind: courseWinds){
- result.put(intToBytes(wind.getID(), 1));
- result.put(longToBytes(wind.getTime(), 6));
- result.put(intToBytes(wind.getRaceID(), 4));
- result.put(intToBytes(wind.getWindDirection(), 2));
- result.put(intToBytes(wind.getWindSpeed(), 2));
- result.put(intToBytes(wind.getBestUpwindAngle(), 2));
- result.put(intToBytes(wind.getBestDownwindAngle(), 2));
- result.put(intToBytes(wind.getFlags(), 1));
- }
- return result.array();
- }
- public static byte[] averageWind(int time, int rawPeriod, int rawSampleSpeed, int period2, int speed2, int period3, int speed3, int period4, int speed4){
- int messageVersionNumber = 0b1;
- byte[] byteTime = longToBytes(time,6);
- byte[] byteRawPeriod = intToBytes(rawPeriod, 2);
- byte[] byteRawSpeed = intToBytes(rawSampleSpeed, 2);
- byte[] bytePeriod2 = intToBytes(period2, 2);
- byte[] byteSpeed2 = intToBytes(speed2, 2);
- byte[] bytePeriod3 = intToBytes(period3, 2);
- byte[] byteSpeed3 = intToBytes(speed3, 2);
- byte[] bytePeriod4 = intToBytes(period4, 2);
- byte[] byteSpeed4 = intToBytes(speed4, 2);
-
- ByteBuffer result = ByteBuffer.allocate(23);
- result.put(intToBytes(messageVersionNumber, 1));
- result.put(byteTime);
- result.put(byteRawPeriod);
- result.put(byteRawSpeed);
- result.put(bytePeriod2);
- result.put(byteSpeed2);
- result.put(bytePeriod3);
- result.put(byteSpeed3);
- result.put(bytePeriod4);
- result.put(byteSpeed4);
- return result.array();
+ return binaryMessageEncoder.getFullMessage();
}
- public static byte[] boatActionMessage(BoatAction boatAction){
- ByteBuffer boatActionMessage = ByteBuffer.allocate(1);
- boatActionMessage.put(intToBytes(boatAction.getBoatAction(), 1));
- byte [] result = boatActionMessage.array();
- return result;
- }
}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/RequestToJoinEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/RequestToJoinEncoder.java
new file mode 100644
index 00000000..e5ef7eee
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/RequestToJoinEncoder.java
@@ -0,0 +1,46 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.RequestToJoin;
+import network.Utils.ByteConverter;
+
+import java.nio.ByteBuffer;
+
+/**
+ * This encoder can encode a {@link network.Messages.RequestToJoin} message.
+ */
+public class RequestToJoinEncoder implements MessageEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public RequestToJoinEncoder() {
+ }
+
+
+ @Override
+ public byte[] encode(AC35Data message) throws InvalidMessageException {
+
+ try {
+
+ //Downcast.
+ RequestToJoin requestToJoin = (RequestToJoin) message;
+
+
+ ByteBuffer requestToJoinBuffer = ByteBuffer.allocate(4);
+
+ requestToJoinBuffer.put(ByteConverter.intToBytes(requestToJoin.getRequestType().getValue(), 4));
+
+ byte[] result = requestToJoinBuffer.array();
+
+ return result;
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode RequestToJoin message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java b/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java
new file mode 100644
index 00000000..c0039d65
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageEncoders/XMLMessageEncoder.java
@@ -0,0 +1,69 @@
+package network.MessageEncoders;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import network.Messages.XMLMessage;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import static network.Utils.ByteConverter.intToBytes;
+import static network.Utils.ByteConverter.longToBytes;
+
+/**
+ * This encoder can encode a {@link XMLMessage} message.
+ */
+public class XMLMessageEncoder implements MessageEncoder {
+
+
+ /**
+ * Constructor.
+ */
+ public XMLMessageEncoder() {
+ }
+
+
+ @Override
+ public byte[] encode(AC35Data message) throws InvalidMessageException {
+
+ try {
+
+ //Downcast.
+ XMLMessage xmlMessage = (XMLMessage) message;
+
+
+ byte[] messageBytes = xmlMessage.getXmlMessage().getBytes(StandardCharsets.UTF_8);
+
+ //Message is 14 + xmlMessage.length bytes.
+ ByteBuffer tempOutputByteBuffer = ByteBuffer.allocate(14 + messageBytes.length);
+
+ //ackNumber converted to bytes
+ byte[] ackNumberBytes = intToBytes(xmlMessage.getAckNumber(), 2);
+
+ //Timestamp converted to bytes.
+ byte[] timestampBytes = longToBytes(xmlMessage.getTimeStamp(), 6);
+
+ //sequenceNumber converted to bytes
+ byte[] sequenceNumberBytes = intToBytes(xmlMessage.getSequenceNumber(), 2);
+
+ //xmlMsgLength converted to bytes
+ byte[] xmlMsgLengthBytes = intToBytes(xmlMessage.getXmlMsgLength(), 2);
+
+
+ tempOutputByteBuffer.put(xmlMessage.getVersionNumber());
+ tempOutputByteBuffer.put(ackNumberBytes);
+ tempOutputByteBuffer.put(timestampBytes);
+ tempOutputByteBuffer.put(xmlMessage.getXmlMsgSubType().getValue());
+ tempOutputByteBuffer.put(sequenceNumberBytes);
+ tempOutputByteBuffer.put(xmlMsgLengthBytes);
+ tempOutputByteBuffer.put(messageBytes);
+
+ return tempOutputByteBuffer.array();
+
+ } catch (Exception e) {
+ throw new InvalidMessageException("Could not encode XMLMessage message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java
new file mode 100644
index 00000000..5b5bcbf5
--- /dev/null
+++ b/racevisionGame/src/main/java/network/MessageRouters/MessageRouter.java
@@ -0,0 +1,131 @@
+package network.MessageRouters;
+
+
+import network.Messages.AC35Data;
+import network.Messages.Enums.MessageType;
+import org.jetbrains.annotations.NotNull;
+import shared.model.RunnableWithFramePeriod;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class routes {@link network.Messages.AC35Data} messages to an appropriate message controller.
+ */
+public class MessageRouter implements RunnableWithFramePeriod {
+
+
+ /**
+ * Incoming queue of messages.
+ */
+ private BlockingQueue incomingMessages;
+
+
+ /**
+ * The routing map, which maps from a {@link MessageType} to a message queue.
+ */
+ private Map> routeMap = new HashMap<>();
+
+
+ /**
+ * The default routing queue.
+ * Messages without routes are sent here.
+ * Nothing by default, which means unrouted messages are discarded
+ */
+ private Optional> defaultRoute = Optional.empty();
+
+
+
+ /**
+ * Constructs a {@link MessageRouter} with a given incoming message queue.
+ * @param incomingMessages Incoming message queue to read from.
+ */
+ public MessageRouter(BlockingQueue incomingMessages) {
+ this.incomingMessages = incomingMessages;
+ }
+
+
+ /**
+ * Returns the queue the message router reads from.
+ * Place messages onto this queue to pass them to the router.
+ * @return Queue the message router reads from.
+ */
+ public BlockingQueue getIncomingMessageQueue() {
+ return incomingMessages;
+ }
+
+
+ /**
+ * Adds a route, which routes a given type of message to a given queue.
+ * @param messageType The message type to route.
+ * @param queue The queue to route messages to.
+ */
+ public void addRoute(MessageType messageType, BlockingQueue queue) {
+ routeMap.put(messageType, queue);
+ }
+
+ /**
+ * Removes the route for a given {@link MessageType}.
+ * @param messageType MessageType to remove route for.
+ */
+ public void removeRoute(MessageType messageType) {
+ routeMap.remove(messageType);
+ }
+
+ /**
+ * Adds a given queue as the default route for any unrouted message types.
+ * @param queue Queue to use as default route.
+ */
+ public void addDefaultRoute(@NotNull BlockingQueue queue) {
+ defaultRoute = Optional.of(queue);
+ }
+
+ /**
+ * Removes the current default route, if it exists.
+ */
+ public void removeDefaultRoute() {
+ defaultRoute = Optional.empty();
+ }
+
+
+
+ @Override
+ public void run() {
+
+ while (!Thread.interrupted()) {
+
+ try {
+
+ AC35Data message = incomingMessages.take();
+
+
+ BlockingQueue queue = routeMap.get(message.getType());
+
+ if (queue != null) {
+ queue.put(message);
+
+ } else {
+ //No route. Use default.
+ BlockingQueue defaultQueue = defaultRoute.orElse(null);
+
+ if (defaultQueue != null) {
+ defaultQueue.put(message);
+ }
+
+ }
+
+
+ } catch (InterruptedException e) {
+ Logger.getGlobal().log(Level.SEVERE, "MessageRouter: " + this + " was interrupted on thread: " + Thread.currentThread() + " while reading message.", e);
+ Thread.currentThread().interrupt();
+
+ }
+
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java b/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java
new file mode 100644
index 00000000..ab33ac1b
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/AssignPlayerBoat.java
@@ -0,0 +1,38 @@
+package network.Messages;
+
+import network.Messages.Enums.MessageType;
+
+
+/**
+ * This is the message the client generates and sends to itself once the server has assigned a boat source ID with {@link JoinAcceptance}.
+ */
+public class AssignPlayerBoat extends AC35Data {
+
+
+ /**
+ * The source ID of the boat assigned to the client.
+ * 0 indicates they haven't been assigned a boat.
+ */
+ private int sourceID = 0;
+
+
+
+
+ /**
+ * Constructs a AssignPlayerBoat message.
+ * @param sourceID The sourceID to assign to the client. 0 indicates no sourceID.
+ */
+ public AssignPlayerBoat(int sourceID){
+ super(MessageType.ASSIGN_PLAYER_BOAT);
+ this.sourceID = sourceID;
+ }
+
+
+ /**
+ * Returns the source ID of the boat assigned to the client.
+ * @return The source ID of the boat assigned to the client.
+ */
+ public int getSourceID() {
+ return sourceID;
+ }
+}
diff --git a/racevisionGame/src/main/java/network/Messages/AverageWind.java b/racevisionGame/src/main/java/network/Messages/AverageWind.java
index 1ba17bd5..1b59f30f 100644
--- a/racevisionGame/src/main/java/network/Messages/AverageWind.java
+++ b/racevisionGame/src/main/java/network/Messages/AverageWind.java
@@ -4,33 +4,187 @@ package network.Messages;
import network.Messages.Enums.MessageType;
/**
- * Created by fwy13 on 25/04/17.
+ * Represents an AverageWind message in the streaming API (see section 4.12).
*/
public class AverageWind extends AC35Data {
- private int msgNum;
- private long lngTime;
- private int rawPeriod;
- private int rawSpeed;
- private int period2;
- private int speed2;
- private int period3;
- private int speed3;
- private int period4;
- private int speed4;
-
- public AverageWind(int msgNum, long lngTime, int rawPeriod, int rawSpeed, int period2, int speed2, int period3, int speed3, int period4, int speed4){
+ /**
+ * The current version number for this message type.
+ */
+ public static final byte currentMessageVersionNumber = 1;
+
+
+ /**
+ * The version number for this message.
+ */
+ private byte messageVersionNumber;
+
+ /**
+ * Timestamp for when the measurement was taken. Milliseconds since unix epoch.
+ */
+ private long time;
+
+ /**
+ * Raw sample rate period. Milliseconds.
+ */
+ private long rawPeriod;
+
+ /**
+ * Raw wind speed, in knots.
+ */
+ private double rawSpeedKnots;
+
+ /**
+ * Wind speed average period for second sample. Milliseconds.
+ */
+ private long sampleTwoPeriod;
+
+ /**
+ * Wind speed of second sample. Knots.
+ */
+ private double sampleTwoSpeedKnots;
+
+ /**
+ * Wind speed average period for third sample. Milliseconds.
+ */
+ private long sampleThreePeriod;
+
+ /**
+ * Wind speed of third sample. Knots.
+ */
+ private double sampleThreeSpeedKnots;
+
+ /**
+ * Wind speed average period for fourth sample. Milliseconds.
+ */
+ private long sampleFourPeriod;
+
+ /**
+ * Wind speed of fourth sample. Knots.
+ */
+ private double sampleFourSpeedKnots;
+
+
+ /**
+ * Creates an AverageWind message, with the given parameters.
+ * @param messageVersionNumber The version number of message.
+ * @param time The timestamp of the message.
+ * @param rawPeriod The period of the raw measurement. Milliseconds.
+ * @param rawSpeedKnots The speed of the raw measurement. Knots.
+ * @param sampleTwoPeriod The period of the second measurement. Milliseconds.
+ * @param sampleTwoSpeedKnots The speed of the second measurement. Knots.
+ * @param sampleThreePeriod The period of the third measurement. Milliseconds.
+ * @param sampleThreeSpeedKnots The speed of the third measurement. Knots.
+ * @param sampleFourPeriod The period of the fourth measurement. Milliseconds.
+ * @param sampleFourSpeedKnots The speed of the fourth measurement. Knots.
+ */
+ public AverageWind(
+ byte messageVersionNumber,
+ long time,
+ long rawPeriod,
+ double rawSpeedKnots,
+ long sampleTwoPeriod,
+ double sampleTwoSpeedKnots,
+ long sampleThreePeriod,
+ double sampleThreeSpeedKnots,
+ long sampleFourPeriod,
+ double sampleFourSpeedKnots ) {
+
super(MessageType.AVGWIND);
- this.msgNum = msgNum;
- this.lngTime = lngTime;
+
+ this.messageVersionNumber = messageVersionNumber;
+ this.time = time;
this.rawPeriod = rawPeriod;
- this.rawSpeed = rawSpeed;
- this.period2 = period2;
- this.speed2 = speed2;
- this.period3 = period3;
- this.speed3 = speed3;
- this.period4 = period4;
- this.speed4 = speed4;
+ this.rawSpeedKnots = rawSpeedKnots;
+ this.sampleTwoPeriod = sampleTwoPeriod;
+ this.sampleTwoSpeedKnots = sampleTwoSpeedKnots;
+ this.sampleThreePeriod = sampleThreePeriod;
+ this.sampleThreeSpeedKnots = sampleThreeSpeedKnots;
+ this.sampleFourPeriod = sampleFourPeriod;
+ this.sampleFourSpeedKnots = sampleFourSpeedKnots;
+ }
+
+
+ /**
+ * Returns the version number of this message.
+ * @return Message version number.
+ */
+ public byte getMessageVersionNumber() {
+ return messageVersionNumber;
+ }
+
+ /**
+ * Returns the timestamp of this message.
+ * @return Timestamp of this message. Milliseconds since unix epoch.
+ */
+ public long getTime() {
+ return time;
+ }
+
+ /**
+ * Returns the period of time over which the raw sample was done. Milliseconds.
+ * @return Raw sample's period.
+ */
+ public long getRawPeriod() {
+ return rawPeriod;
+ }
+
+ /**
+ * Returns the wind speed of the raw sample. Knots.
+ * @return Wind speed of raw sample. Knots
+ */
+ public double getRawSpeedKnots() {
+ return rawSpeedKnots;
}
+ /**
+ * Returns the period of time over which the second sample was done. Milliseconds.
+ * @return Second sample's period.
+ */
+ public long getSampleTwoPeriod() {
+ return sampleTwoPeriod;
+ }
+
+ /**
+ * Returns the wind speed of the second sample. Knots.
+ * @return Wind speed of second sample. Knots
+ */
+ public double getSampleTwoSpeedKnots() {
+ return sampleTwoSpeedKnots;
+ }
+
+ /**
+ * Returns the period of time over which the third sample was done. Milliseconds.
+ * @return Third sample's period.
+ */
+ public long getSampleThreePeriod() {
+ return sampleThreePeriod;
+ }
+
+ /**
+ * Returns the wind speed of the third sample. Knots.
+ * @return Wind speed of third sample. Knots
+ */
+ public double getSampleThreeSpeedKnots() {
+ return sampleThreeSpeedKnots;
+ }
+
+ /**
+ * Returns the period of time over which the fourth sample was done. Milliseconds.
+ * @return Fourth sample's period.
+ */
+ public long getSampleFourPeriod() {
+ return sampleFourPeriod;
+ }
+
+ /**
+ * Returns the wind speed of the fourth sample. Knots.
+ * @return Wind speed of fourth sample. Knots
+ */
+ public double getSampleFourSpeedKnots() {
+ return sampleFourSpeedKnots;
+ }
+
+
+
}
diff --git a/racevisionGame/src/main/java/network/Messages/BoatAction.java b/racevisionGame/src/main/java/network/Messages/BoatAction.java
index d20943a5..93c6a310 100644
--- a/racevisionGame/src/main/java/network/Messages/BoatAction.java
+++ b/racevisionGame/src/main/java/network/Messages/BoatAction.java
@@ -4,19 +4,51 @@ import network.Messages.Enums.BoatActionEnum;
import network.Messages.Enums.MessageType;
/**
- * Created by David on 10/07/2017.
+ * Represents a BoatAction message.
*/
public class BoatAction extends AC35Data {
- private byte boatAction;
+ /**
+ * The action for this message.
+ */
+ private BoatActionEnum boatAction;
+
+ /**
+ * The source ID of the boat this action relates to.
+ */
+ private int sourceID = 0;
+
+ /**
+ * Constructs a BoatActon message with a given action.
+ * @param boatAction Action to use.
+ */
public BoatAction(BoatActionEnum boatAction){
super(MessageType.BOATACTION);
- this.boatAction = boatAction.getValue();
+ this.boatAction = boatAction;
}
- public byte getBoatAction() {
+ /**
+ * Returns the action for this message.
+ * @return The action for this message.
+ */
+ public BoatActionEnum getBoatAction() {
return boatAction;
}
-}
\ No newline at end of file
+ /**
+ * Returns the boat source ID for this message.
+ * @return The source ID for this message.
+ */
+ public int getSourceID() {
+ return sourceID;
+ }
+
+ /**
+ * Sets the boat source ID for this message.
+ * @param sourceID The source for this message.
+ */
+ public void setSourceID(int sourceID) {
+ this.sourceID = sourceID;
+ }
+}
diff --git a/racevisionGame/src/main/java/network/Messages/BoatLocation.java b/racevisionGame/src/main/java/network/Messages/BoatLocation.java
index 301584b4..90529d96 100644
--- a/racevisionGame/src/main/java/network/Messages/BoatLocation.java
+++ b/racevisionGame/src/main/java/network/Messages/BoatLocation.java
@@ -1,130 +1,188 @@
package network.Messages;
+import network.Messages.Enums.BoatLocationDeviceEnum;
import network.Messages.Enums.MessageType;
import network.Utils.AC35UnitConverter;
-import shared.model.Constants;
+import shared.model.Azimuth;
+import shared.model.Bearing;
-import static network.Utils.AC35UnitConverter.convertGPS;
-import static network.Utils.AC35UnitConverter.convertGPSToInt;
+import static network.Utils.AC35UnitConverter.unpackGPS;
/**
* Represents the information in a boat location message (AC streaming spec: 4.9).
*/
public class BoatLocation extends AC35Data {
- //TODO move these to an enum.
- public static final byte Unknown = 0;
- public static final byte RacingYacht = 1;
- public static final byte CommitteeBoat = 2;
- public static final byte Mark = 3;
- public static final byte Pin = 4;
- public static final byte ChaseBoat = 5;
- public static final byte MedicalBoat = 6;
- public static final byte MarshallBoat = 7;
- public static final byte UmpireBoat = 8;
- public static final byte UmpireSoftwareApplication = 9;
- public static final byte PrincipalRaceOfficerApplication = 10;
- public static final byte WeatherStation = 11;
- public static final byte Helicopter = 12;
- public static final byte DataProcessingApplication = 13;
-
- ///Version number of the message - is always 1.
- private byte messageVersionNumber = 1;
- ///Time of the event - milliseconds since jan 1 1970. Proper type is 6 byte int.
+
+ /**
+ * The current messageVersionNumber according to the API spec.
+ */
+ public static final byte currentMessageVersionNumber = 1;
+
+
+ /**
+ * Version number of the message.
+ */
+ private byte messageVersionNumber;
+
+ /**
+ * Time of the event - milliseconds since jan 1 1970. Proper type is 6 byte int.
+ */
private long time;
- ///Source ID of the boat.
+
+ /**
+ * Source ID of the boat.
+ */
private int sourceID;
- ///Sequence number of the message.
+
+ /**
+ * Sequence number of the message.
+ */
private long sequenceNumber;
- ///Device type of the message (physical source of the message).
- private byte deviceType;
- ///Latitude of the boat.
- private int latitude;
- ///Longitude of the boat.
- private int longitude;
+ /**
+ * Device type of the message (physical source of the message).
+ */
+ private BoatLocationDeviceEnum deviceType;
+
+ /**
+ * Latitude of the boat.
+ */
+ private double latitude;
- ///Altitude of the boat.
+ /**
+ * Longitude of the boat.
+ */
+ private double longitude;
+
+ /**
+ * Altitude of the boat.
+ */
private int altitude;
- ///Heading of the boat. Clockwise, 0 = north. Proper type is unsigned 2 byte int.
- private int heading;
+ /**
+ * Heading of the boat. Clockwise, 0 = north. Proper type is unsigned 2 byte int.
+ */
+ private Bearing heading;
- ///Pitch of the boat.
+ /**
+ * Pitch of the boat.
+ */
private short pitch;
- ///Roll of the boat.
+ /**
+ * Roll of the boat.
+ */
private short roll;
- ///Speed of the boat. Proper type is unsigned 2 byte int. millimeters per second.
- private int boatSpeed;
-
- ///Course over ground (COG) of the boat. Proper type is unsigned 2 byte int.
- private int boatCOG;
-
- ///Speed over ground (SOG) of the boat. Proper type is unsigned 2 byte int. millimeters per second.
- private int boatSOG;
+ /**
+ * Speed of the boat, in knots.
+ */
+ private double boatSpeedKnots;
- ///Apparent wind speed at time of event. Proper type is unsigned 2 byte int. millimeters per second.
- private int apparentWindSpeed;
+ /**
+ * Course over ground (COG) of the boat.
+ */
+ private Bearing boatCOG;
- ///Apparent wind angle at time of the event. Wind over starboard = positive.
- private short apparentWindAngle;
+ /**
+ * Speed over ground (SOG) of the boat, in knots.
+ */
+ private double boatSOGKnots;
- ///True wind speed. Proper type is unsigned 2 byte int. millimeters per second.
- private int trueWindSpeed;
+ /**
+ * Apparent wind speed at time of event. Proper type is unsigned 2 byte int. millimeters per second.
+ */
+ private double apparentWindSpeedKnots;
- ///True wind direction. Proper type is unsigned 2 byte int. 0x0000 = North, etc..
- private int trueWindDirection;
+ /**
+ * Apparent wind angle at time of the event. Wind over starboard = positive.
+ */
+ private Azimuth apparentWindAngle;
- ///True wind angle. Clockwise compass direction, 0 = north.
- private short trueWindAngle;
+ /**
+ * True wind speed, in knots.
+ */
+ private double trueWindSpeedKnots;
- ///Current drift. Proper type is unsigned 2 byte int. millimeters per second.
- private int currentDrift;
+ /**
+ * True wind direction.
+ */
+ private Bearing trueWindDirection;
- ///Current set. Proper type is unsigned 2 byte int. Clockwise compass direction, 0 = north.
- private int currentSet;
+ /**
+ * True wind angle. Clockwise compass direction, 0 = north.
+ */
+ private Azimuth trueWindAngle;
- ///Rudder angle. Positive is rudder set to turn yacht to port.
- private short rudderAngle;
+ /**
+ * Current drift, in knots.
+ */
+ private double currentDriftKnots;
+ /**
+ * Current set.
+ */
+ private Bearing currentSet;
/**
- * Ctor. Default.
+ * Rudder angle. Positive is rudder set to turn yacht to port.
*/
- public BoatLocation() {
- super(MessageType.BOATLOCATION);
- }
+ private Azimuth rudderAngle;
+
+
/**
- * Ctor, with all parameters.
+ * Constructs a BoatLocation message with the given parameters.
*
* @param messageVersionNumber message number
* @param time time of message
* @param sourceID id of boat
* @param sequenceNumber number of boat message
- * @param deviceType type of boat
+ * @param deviceType The source of the BoatLocation message.
* @param latitude lat of boat
* @param longitude lon of boat
* @param altitude altitude of boat
* @param heading heading of boat
* @param pitch pitch of boat
* @param roll roll of boat
- * @param boatSpeed boats speed
+ * @param boatSpeedKnots boats speed
* @param boatCOG boat cog
- * @param boatSOG boat sog
- * @param apparentWindSpeed wind speed
+ * @param boatSOGKnots boat sog
+ * @param apparentWindSpeedKnots wind speed
* @param apparentWindAngle wind angle
- * @param trueWindSpeed true wind speed
+ * @param trueWindSpeedKnots true wind speed
* @param trueWindDirection true wind direction
* @param trueWindAngle true wind angle
- * @param currentDrift current drift
+ * @param currentDriftKnots current drift
* @param currentSet current set
* @param rudderAngle rudder angle
*/
- public BoatLocation(byte messageVersionNumber, long time, int sourceID, long sequenceNumber, byte deviceType, int latitude, int longitude, int altitude, int heading, short pitch, short roll, int boatSpeed, int boatCOG, int boatSOG, int apparentWindSpeed, short apparentWindAngle, int trueWindSpeed, int trueWindDirection, short trueWindAngle, int currentDrift, int currentSet, short rudderAngle) {
+ public BoatLocation(
+ byte messageVersionNumber,
+ long time,
+ int sourceID,
+ long sequenceNumber,
+ BoatLocationDeviceEnum deviceType,
+ double latitude,
+ double longitude,
+ int altitude,
+ Bearing heading,
+ short pitch,
+ short roll,
+ double boatSpeedKnots,
+ Bearing boatCOG,
+ double boatSOGKnots,
+ double apparentWindSpeedKnots,
+ Azimuth apparentWindAngle,
+ double trueWindSpeedKnots,
+ Bearing trueWindDirection,
+ Azimuth trueWindAngle,
+ double currentDriftKnots,
+ Bearing currentSet,
+ Azimuth rudderAngle ) {
+
super(MessageType.BOATLOCATION);
this.messageVersionNumber = messageVersionNumber;
@@ -138,348 +196,251 @@ public class BoatLocation extends AC35Data {
this.heading = heading;
this.pitch = pitch;
this.roll = roll;
- this.boatSpeed = boatSpeed;
+ this.boatSpeedKnots = boatSpeedKnots;
this.boatCOG = boatCOG;
- this.boatSOG = boatSOG;
- this.apparentWindSpeed = apparentWindSpeed;
+ this.boatSOGKnots = boatSOGKnots;
+ this.apparentWindSpeedKnots = apparentWindSpeedKnots;
this.apparentWindAngle = apparentWindAngle;
- this.trueWindSpeed = trueWindSpeed;
+ this.trueWindSpeedKnots = trueWindSpeedKnots;
this.trueWindDirection = trueWindDirection;
this.trueWindAngle = trueWindAngle;
- this.currentDrift = currentDrift;
+ this.currentDriftKnots = currentDriftKnots;
this.currentSet = currentSet;
this.rudderAngle = rudderAngle;
}
- public BoatLocation(int sourceID, double lat, double lon, long sequenceNumber, double heading, double boatSpeed, long time) {
- super(MessageType.BOATLOCATION);
-
- this.messageVersionNumber = (byte) 1;
- this.time = time;
- this.sourceID = sourceID;
- this.sequenceNumber = sequenceNumber;
- this.deviceType = 1;
- this.latitude = convertGPSToInt(lat);
- this.longitude = convertGPSToInt(lon);
- this.altitude = 0;
- this.heading = convertHeadingDoubleToInt(heading);
- this.pitch = 0;
- this.roll = 0;
- this.boatSpeed = convertBoatSpeedDoubleToInt(boatSpeed);
- this.boatCOG = 0;
- this.boatSOG = convertBoatSpeedDoubleToInt(boatSpeed);
- this.apparentWindSpeed = 0;
- this.apparentWindAngle = 0;
- this.trueWindSpeed = 0;
- this.trueWindDirection = 0;
- this.trueWindAngle = 0;
- this.currentDrift = 0;
- this.currentSet = 0;
- this.rudderAngle = 0;
- }
-
-
- //Getters and setters for message properties.
-
- /**
- * Converts a double representing a latitude or longitude coordinate to an int, as required by the streaming spec format.
- *
- * @param coordinate Latitude or longitude to convert. Double.
- * @return int representation of coordinate.
- */
- public static int convertCoordinateDoubleToInt(double coordinate) {
- int coordinateInt = (int) ((coordinate / 180.0) * 2147483648.0);
-
- return coordinateInt;
- }
-
- /**
- * Converts an int representing a latitude or longitude coordinate to a double, as required by the streaming spec format.
- *
- * @param coordinate Latitude or longitude to convert. int.
- * @return double representation of coordinate.
- */
- public static double convertCoordinateIntToDouble(int coordinate) {
- double coordinateDouble = (double) ((coordinate * 180.0) / 2147483648.0);
-
- return coordinateDouble;
- }
-
- /**
- * Converts an int representing a heading to a double, as required by the streaming spec format.
- *
- * @param heading Heading to convert. int.
- * @return double representation of heading.
- */
- public static double convertHeadingIntToDouble(int heading) {
-
- double headingDouble = (double) ((heading * 360.0) / 65536.0);
-
- return headingDouble;
- }
-
- /**
- * Converts a double representing a heading to an int, as required by the streaming spec format.
- *
- * @param heading Heading to convert. double.
- * @return int representation of heading.
- */
- public static int convertHeadingDoubleToInt(double heading) {
-
- int headingInt = (int) ((heading * 65536.0) / 360.0);
-
- return headingInt;
- }
-
- /**
- * Converts a short representing the wind's true angle to a double, as required by the streaming spec format.
- *
- * @param angle Angle to convert. short.
- * @return double representation of heading.
- */
- public static double convertTrueWindAngleShortToDouble(short angle) {
-
- double angleDouble = (double) ((angle * 180.0) / 32768.0);
- return angleDouble;
+ public BoatLocation(
+ int sourceID,
+ double lat,
+ double lon,
+ long sequenceNumber,
+ BoatLocationDeviceEnum deviceType,
+ Bearing heading,
+ double boatSpeedKnots,
+ long time ) {
+
+ this(
+ BoatLocation.currentMessageVersionNumber,
+ time,
+ sourceID,
+ sequenceNumber,
+ deviceType,
+ lat,
+ lon,
+ 0,
+ heading,
+ (short) 0,
+ (short) 0,
+ boatSpeedKnots,
+ heading,
+ boatSpeedKnots,
+ 0,
+ Azimuth.fromDegrees(0),
+ 0,
+ Bearing.fromDegrees(0),
+ Azimuth.fromDegrees(0),
+ 0,
+ Bearing.fromDegrees(0),
+ Azimuth.fromDegrees(0) );
}
- /**
- * Converts a double representing the wind's true angle to a short, as required by the streaming spec format.
- *
- * @param angle Angle to convert. double.
- * @return short representation of heading.
- */
- public static short convertTrueWindAngleDoubleToShort(double angle) {
-
- short angleShort = (short) ((angle / 180.0) * 32768.0);
-
- return angleShort;
- }
/**
- * Converts a double representing the speed of a boat in knots to an int in millimeters per second, as required by the streaming spec format.
- *
- * @param speed Speed in knots, stored as a double.
- * @return Speed in millimeters per second, stored as an int (using only the two least significant bytes).
+ * Returns the version number of the message.
+ * @return The version number of the message.
*/
- public static int convertBoatSpeedDoubleToInt(double speed) {
- //Calculate millimeters per second.
- double millimetersPerSecond = speed * Constants.KnotsToMMPerSecond;
-
- //Convert to an int.
- int millimetersPerSecondInt = (int) Math.round(millimetersPerSecond);
-
- return millimetersPerSecondInt;
- }
-
- /**
- * Converts an int representing the speed of a boat in millimeters per second to a double in knots, as required by the streaming spec format.
- *
- * @param speed Speed in millimeters per second, stored as an int.
- * @return Speed in knots, stored as a double.
- */
- public static double convertBoatSpeedIntToDouble(int speed) {
-
- //Calculate knots.
- double knots = speed / Constants.KnotsToMMPerSecond;
-
- return knots;
- }
-
public byte getMessageVersionNumber() {
return messageVersionNumber;
}
- public void setMessageVersionNumber(byte messageVersionNumber) {
- this.messageVersionNumber = messageVersionNumber;
- }
+ /**
+ * Returns the time that this message was generated at.
+ * @return Time message was generated at, in milliseconds since unix epoch.
+ */
public long getTime() {
return time;
}
- public void setTime(long time) {
- this.time = time;
- }
-
+ /**
+ * Returns the sourceID of the boat this message relates to.
+ * @return SourceID of the boat this message relates to.
+ */
public int getSourceID() {
return sourceID;
}
- public void setSourceID(int sourceID) {
- this.sourceID = sourceID;
- }
-
+ /**
+ * Returns the sequence number of this message.
+ * @return The sequence number of the message.
+ */
public long getSequenceNumber() {
return sequenceNumber;
}
- public void setSequenceNumber(long sequenceNumber) {
- this.sequenceNumber = sequenceNumber;
- }
- public byte getDeviceType() {
+ /**
+ * Returns the device source of this message.
+ * @return The device source of this message.
+ */
+ public BoatLocationDeviceEnum getDeviceType() {
return deviceType;
}
- public void setDeviceType(byte deviceType) {
- this.deviceType = deviceType;
- }
- public int getLatitude() {
+ /**
+ * Returns the latitude, in degrees, that the boat is located at.
+ * @return Latitude, in degrees, of boat.
+ */
+ public double getLatitude() {
return latitude;
}
- public void setLatitude(int latitude) {
- this.latitude = latitude;
- }
- public int getLongitude() {
+ /**
+ * Returns the longitude, in degrees, that the boat is located at.
+ * @return Longitude, in degrees, of boat.
+ */
+ public double getLongitude() {
return longitude;
}
- public double getLatitudeDouble(){
- return convertGPS(this.latitude);
- }
-
- public double getLongitudeDouble(){
- return convertGPS(this.longitude);
- }
-
- public void setLongitude(int longitude) {
- this.longitude = longitude;
- }
-
+ /**
+ * Returns the altitude of the boat.
+ * @return The altitude of the boat.
+ */
public int getAltitude() {
return altitude;
}
- public void setAltitude(int altitude) {
- this.altitude = altitude;
- }
- public int getHeading() {
+ /**
+ * Returns the current heading/bearing of the boat.
+ * @return Heading of the boat.
+ */
+ public Bearing getHeading() {
return heading;
}
- public void setHeading(int heading) {
- this.heading = heading;
- }
+ /**
+ * Returns the current pitch of the boat.
+ * @return Pitch of the boat.
+ */
public short getPitch() {
return pitch;
}
- public void setPitch(short pitch) {
- this.pitch = pitch;
- }
+ /**
+ * Returns the current roll of the boat.
+ * @return Roll of the boat.
+ */
public short getRoll() {
return roll;
}
- public void setRoll(short roll) {
- this.roll = roll;
- }
- public int getBoatSpeed() {
- return boatSpeed;
+ /**
+ * Returns the current boat speed, in knots.
+ * @return Current boat speed, in knots.
+ */
+ public double getBoatSpeedKnots() {
+ return boatSpeedKnots;
}
- public void setBoatSpeed(int boatSpeed) {
- this.boatSpeed = boatSpeed;
- }
- public int getBoatCOG() {
+ /**
+ * Returns the boat's Course Over Ground.
+ * @return Boat's COG.
+ */
+ public Bearing getBoatCOG() {
return boatCOG;
}
- public void setBoatCOG(int boatCOG) {
- this.boatCOG = boatCOG;
- }
-
- public int getBoatSOG() {
- return boatSOG;
- }
- public void setBoatSOG(int boatSOG) {
- this.boatSOG = boatSOG;
+ /**
+ * Returns the boats Speed Over Ground, in knots.
+ * @return Boat's SOG.
+ */
+ public double getBoatSOGKnots() {
+ return boatSOGKnots;
}
- public int getApparentWindSpeed() {
- return apparentWindSpeed;
- }
- public void setApparentWindSpeed(int apparentWindSpeed) {
- this.apparentWindSpeed = apparentWindSpeed;
+ /**
+ * Returns the apparent wind speed, in knots, at the boat.
+ * @return Wind speed, in knots, at the boat.
+ */
+ public double getApparentWindSpeedKnots() {
+ return apparentWindSpeedKnots;
}
- public short getApparentWindAngle() {
+ /**
+ * Returns the apparent wind angle at the boat.
+ * @return Wind angle at the boat.
+ */
+ public Azimuth getApparentWindAngle() {
return apparentWindAngle;
}
- public void setApparentWindAngle(short apparentWindAngle) {
- this.apparentWindAngle = apparentWindAngle;
- }
- public int getTrueWindSpeed() {
- return trueWindSpeed;
+ /**
+ * Returns the true wind speed, in knots.
+ * @return True wind speed, in knots.
+ */
+ public double getTrueWindSpeedKnots() {
+ return trueWindSpeedKnots;
}
- public void setTrueWindSpeed(int trueWindSpeed) {
- this.trueWindSpeed = trueWindSpeed;
- }
- public int getTrueWindDirection()
+ /**
+ * Returns the true wind direction.
+ * @return True wind direction.
+ */
+ public Bearing getTrueWindDirection()
{
return trueWindDirection;
}
- public void setTrueWindDirection(int trueWindDirection)
- {
- this.trueWindDirection = trueWindDirection;
- }
- public short getTrueWindAngle() {
+ /**
+ * Returns the true wind angle.
+ * @return True wind angle.
+ */
+ public Azimuth getTrueWindAngle() {
return trueWindAngle;
}
- public void setTrueWindAngle(short trueWindAngle) {
- this.trueWindAngle = trueWindAngle;
- }
- public int getCurrentDrift() {
- return currentDrift;
+ /**
+ * Returns the current drift of the boat, in knots.
+ * @return Current drift, in knots.
+ */
+ public double getCurrentDriftKnots() {
+ return currentDriftKnots;
}
- public void setCurrentDrift(int currentDrift) {
- this.currentDrift = currentDrift;
- }
- public int getCurrentSet() {
+ /**
+ * Returns the current set of the boat.
+ * @return Current set of the boat.
+ */
+ public Bearing getCurrentSet() {
return currentSet;
}
- public void setCurrentSet(int currentSet) {
- this.currentSet = currentSet;
- }
- public short getRudderAngle() {
+ /**
+ * Returns the current rudder angle of the boat.
+ * @return Current rudder angle of the boat.
+ */
+ public Azimuth getRudderAngle() {
return rudderAngle;
}
- public void setRudderAngle(short rudderAngle) {
- this.rudderAngle = rudderAngle;
- }
-
- public double getHeadingDegrees(){
- return AC35UnitConverter.convertHeading(getHeading());
- }
- public double getTrueWindAngleDegrees(){
- return AC35UnitConverter.convertTrueWindAngle(getTrueWindAngle());
- }
@Override
public String toString() {
@@ -519,28 +480,28 @@ public class BoatLocation extends AC35Data {
builder.append(this.getRoll());
builder.append("\nBoat speed (mm/sec): ");
- builder.append(this.getBoatSpeed());
+ builder.append(this.getBoatSpeedKnots());
builder.append("\nBoat COG: ");
builder.append(this.getBoatCOG());
builder.append("\nBoat SOG: ");
- builder.append(this.getBoatSOG());
+ builder.append(this.getBoatSOGKnots());
builder.append("\nApparent wind speed: ");
- builder.append(this.getApparentWindSpeed());
+ builder.append(this.getApparentWindSpeedKnots());
builder.append("\nApparent wind angle: ");
builder.append(this.getApparentWindAngle());
builder.append("\nTrue wind speed: ");
- builder.append(this.getTrueWindSpeed());
+ builder.append(this.getTrueWindSpeedKnots());
builder.append("\nTrue wind angle: ");
builder.append(this.getTrueWindAngle());
builder.append("\nCurrent drift: ");
- builder.append(this.getCurrentDrift());
+ builder.append(this.getCurrentDriftKnots());
builder.append("\nCurrent set: ");
builder.append(this.getCurrentSet());
diff --git a/racevisionGame/src/main/java/network/Messages/BoatStatus.java b/racevisionGame/src/main/java/network/Messages/BoatStatus.java
index 54996726..7fa9228f 100644
--- a/racevisionGame/src/main/java/network/Messages/BoatStatus.java
+++ b/racevisionGame/src/main/java/network/Messages/BoatStatus.java
@@ -5,22 +5,62 @@ import network.Messages.Enums.BoatStatusEnum;
import network.Utils.ByteConverter;
/**
- * Created by hba56 on 23/04/17.
+ * Represents the information in a BoatStatus message, which is contained inside a RaceStatus message (AC streaming spec: 4.2).
*/
public class BoatStatus {
+ /**
+ * The sourceID of the boat.
+ */
private int sourceID;
- private byte boatStatus;
+
+ /**
+ * The status of the boat.
+ */
+ private BoatStatusEnum boatStatus;
+
+ /**
+ * The leg number that the boat is on.
+ */
private byte legNumber;
+
+ /**
+ * The number of penalties awarded to the boat.
+ */
private byte numPenaltiesAwarded;
+
+ /**
+ * The number of penalties served by the boat.
+ */
private byte numPenaltiesServed;
+
+ /**
+ * The time at which it is estimated the boat will reach the next mark.
+ * Milliseconds since unix epoch.
+ */
private long estTimeAtNextMark;
+
+ /**
+ * The time at which it is estimated the boat will finish the race.
+ * Milliseconds since unix epoch.
+ */
private long estTimeAtFinish;
- public BoatStatus(int sourceID, byte boatStatus, byte legNumber, byte numPenaltiesAwarded, byte numPenaltiesServed, long estTimeAtNextMark, long estTimeAtFinish) {
+
+ /**
+ * Constructs a BoatStatus message with the given parameters.
+ * @param sourceID The sourceID of the boat.
+ * @param boatStatus The status of the boat.
+ * @param legNumber The leg number the boat is on.
+ * @param numPenaltiesAwarded The number of penalties awarded to the boat.
+ * @param numPenaltiesServed The number of penalties served by the boat.
+ * @param estTimeAtNextMark The estimated time at which the boat will reach the next mark.
+ * @param estTimeAtFinish The estimated time at which the boat will finish the race.
+ */
+ public BoatStatus(int sourceID, BoatStatusEnum boatStatus, int legNumber, byte numPenaltiesAwarded, byte numPenaltiesServed, long estTimeAtNextMark, long estTimeAtFinish) {
this.sourceID = sourceID;
this.boatStatus = boatStatus;
- this.legNumber = legNumber;
+ this.legNumber = ByteConverter.intToBytes(legNumber, 1)[0];
this.numPenaltiesAwarded = numPenaltiesAwarded;
this.numPenaltiesServed = numPenaltiesServed;
this.estTimeAtNextMark = estTimeAtNextMark;
@@ -28,41 +68,77 @@ public class BoatStatus {
}
- public BoatStatus(int sourceID, BoatStatusEnum boatStatusEnum, int legNum, long estTimeAtNextMark) {
- this.sourceID = sourceID;
- this.boatStatus = boatStatusEnum.getValue();
- this.legNumber = ByteConverter.intToBytes(legNum)[0];
- this.numPenaltiesAwarded = 0;
- this.numPenaltiesServed = 0;
- this.estTimeAtFinish = 0;
- this.estTimeAtNextMark = estTimeAtNextMark;
+ /**
+ * Constructs a BoatStatus message with the given parameters. Sets penalties to zero, and time at finish to zero.
+ * @param sourceID The sourceID of the boat.
+ * @param boatStatus The status of the boat.
+ * @param legNumber The leg number the boat is on.
+ * @param estTimeAtNextMark The estimated time at which the boat will reach the next mark.
+ */
+ public BoatStatus(int sourceID, BoatStatusEnum boatStatus, int legNumber, long estTimeAtNextMark) {
+ this(
+ sourceID,
+ boatStatus,
+ legNumber,
+ (byte) 0,
+ (byte) 0,
+ estTimeAtNextMark,
+ 0 );
}
+ /**
+ * Returns the sourceID of the boat.
+ * @return The sourceID of the boat.
+ */
public int getSourceID() {
return sourceID;
}
- public byte getBoatStatus() {
+ /**
+ * Returns the status of the boat.
+ * @return The status of the boat.
+ */
+ public BoatStatusEnum getBoatStatus() {
return boatStatus;
}
+ /**
+ * Returns the leg number of boat is on.
+ * @return The leg number of boat is on.
+ */
public byte getLegNumber() {
return legNumber;
}
+ /**
+ * Returns the number of penalties awarded to the boat.
+ * @return Number of penalties awarded to boat.
+ */
public byte getNumPenaltiesAwarded() {
return numPenaltiesAwarded;
}
+ /**
+ * Returns the number of penalties served by the boat.
+ * @return The number of penalties served by the boat.
+ */
public byte getNumPenaltiesServed() {
return numPenaltiesServed;
}
+ /**
+ * Returns the time at which it is estimated the boat will reach the next mark. Milliseconds since unix epoch.
+ * @return Time at which boat will reach next mark.
+ */
public long getEstTimeAtNextMark() {
return estTimeAtNextMark;
}
+ /**
+ * Returns the time at which it is estimated the boat will finish the race. Milliseconds since unix epoch.
+ * @return Time at which boat will finish the race.
+ */
public long getEstTimeAtFinish() {
return estTimeAtFinish;
}
diff --git a/racevisionGame/src/main/java/network/Messages/CourseWind.java b/racevisionGame/src/main/java/network/Messages/CourseWind.java
index 727d5fcc..60575edb 100644
--- a/racevisionGame/src/main/java/network/Messages/CourseWind.java
+++ b/racevisionGame/src/main/java/network/Messages/CourseWind.java
@@ -2,57 +2,132 @@ package network.Messages;
import network.Messages.Enums.MessageType;
+import shared.model.Bearing;
/**
- * Created by fwy13 on 21/04/17.
+ * Contains a single CourseWind record.
+ * A CourseWinds message contains one or more CourseWind messages.
*/
public class CourseWind extends AC35Data {
- private int ID, raceID, windDirection, windSpeed, bestUpwindAngle, bestDownwindAngle, flags;
+ /**
+ * The ID for this wind source.
+ */
+ private int ID;
+
+ /**
+ * The time the wind was captured at. Milliseconds since unix epoch.
+ */
private long time;
- public CourseWind(int ID, long time, int raceID, int windDirection, int windSpeed, int bestUpwindAngle, int bestDownwindAngle,
- int flags){
+ /**
+ * The ID of the race this applies to.
+ * 0 means it isn't race specific.
+ */
+ private int raceID;
+
+ /**
+ * Direction of the wind.
+ */
+ private Bearing windDirection;
+
+ /**
+ * The speed of the wind, in knots.
+ */
+ private double windSpeedKnots;
+
+ /**
+ * Optimum upwind sailing angle.
+ */
+ private Bearing bestUpwindAngle;
+
+ /**
+ * Optimum downwind sailing angle.
+ */
+ private Bearing bestDownwindAngle;
+
+ /**
+ * Various flags which determine which values are valid.
+ */
+ private short flags;
+
+
+
+ public CourseWind(int ID, long time, int raceID, Bearing windDirection, double windSpeedKnots, Bearing bestUpwindAngle, Bearing bestDownwindAngle, short flags) {
super(MessageType.COURSEWIND);
this.ID = ID;
this.time = time;
this.raceID = raceID;
this.windDirection = windDirection;
- this.windSpeed = windSpeed;
+ this.windSpeedKnots = windSpeedKnots;
this.bestUpwindAngle = bestUpwindAngle;
this.bestDownwindAngle = bestDownwindAngle;
this.flags = flags;
}
+
+ /**
+ * Returns the ID of the wind source.
+ * @return ID of the wind source.
+ */
public int getID() {
return ID;
}
+ /**
+ * Returns the time that this was captured at. Milliseconds since unix epoch.
+ * @return Time this wind was captured at.
+ */
+ public long getTime() {
+ return time;
+ }
+
+ /**
+ * Returns the ID of the race this wind source belongs to. 0 means any race.
+ * @return ID of the race this belongs to.
+ */
public int getRaceID() {
return raceID;
}
- public int getWindDirection() {
+ /**
+ * Returns the direction of the wind.
+ * @return The direction of the wind.
+ */
+ public Bearing getWindDirection() {
return windDirection;
}
- public int getWindSpeed() {
- return windSpeed;
+ /**
+ * Returns the wind speed, in knots.
+ * @return Wind speed, in knots.
+ */
+ public double getWindSpeedKnots() {
+ return windSpeedKnots;
}
- public int getBestUpwindAngle() {
+ /**
+ * Returns the best upwind sailing angle.
+ * @return Best upwind sailing angle.
+ */
+ public Bearing getBestUpwindAngle() {
return bestUpwindAngle;
}
- public int getBestDownwindAngle() {
+ /**
+ * Returns the best downwind sailing angle.
+ * @return The best downwind sailing angle.
+ */
+ public Bearing getBestDownwindAngle() {
return bestDownwindAngle;
}
+ /**
+ * Returns various flags which determine which values are valid.
+ * @return Flag which determines which values are valid.
+ */
public int getFlags() {
return flags;
}
- public long getTime() {
- return time;
- }
}
diff --git a/racevisionGame/src/main/java/network/Messages/CourseWinds.java b/racevisionGame/src/main/java/network/Messages/CourseWinds.java
index fc575867..d62f3135 100644
--- a/racevisionGame/src/main/java/network/Messages/CourseWinds.java
+++ b/racevisionGame/src/main/java/network/Messages/CourseWinds.java
@@ -6,19 +6,67 @@ import network.Messages.Enums.MessageType;
import java.util.List;
/**
- * Created by fwy13 on 25/04/17.
+ * Represents the information in a CourseWind message (AC streaming spec: 4.11).
*/
public class CourseWinds extends AC35Data {
- private int msgVerNum;
- private int selectedWindID;
+ /**
+ * The current version number for this message type.
+ */
+ public static final byte currentMessageVersionNumber = 1;
+
+
+ /**
+ * The version number of this message.
+ */
+ private byte messageVersionNumber;
+
+ /**
+ * The ID of the wind source currently selected.
+ */
+ private byte selectedWindID;
+
+ /**
+ * A list of wind sources.
+ */
private List courseWinds;
- public CourseWinds(int msgVerNum, int selectedWindID, List courseWinds){
+
+ /**
+ * Constructs a CourseWinds with given parameters.
+ * @param messageVersionNumber The version number of the message.
+ * @param selectedWindID The selected wind ID.
+ * @param courseWinds A list of wind sources.
+ */
+ public CourseWinds(byte messageVersionNumber, byte selectedWindID, List courseWinds) {
super(MessageType.COURSEWIND);
- this.msgVerNum = msgVerNum;
+ this.messageVersionNumber = messageVersionNumber;
this.selectedWindID = selectedWindID;
this.courseWinds = courseWinds;
}
+
+ /**
+ * Returns the version number of this message.
+ * @return Version number of this message.
+ */
+ public byte getMessageVersionNumber() {
+ return messageVersionNumber;
+ }
+
+ /**
+ * Returns the ID of the selected wind source.
+ * @return ID of the selected wind source.
+ */
+ public byte getSelectedWindID() {
+ return selectedWindID;
+ }
+
+ /**
+ * Returns the list of wind sources.
+ * @return List of wind sources.
+ */
+ public List getCourseWinds() {
+ return courseWinds;
+ }
}
diff --git a/racevisionGame/src/main/java/network/Messages/Enums/BoatActionEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/BoatActionEnum.java
index 84f6e0fd..372349f7 100644
--- a/racevisionGame/src/main/java/network/Messages/Enums/BoatActionEnum.java
+++ b/racevisionGame/src/main/java/network/Messages/Enums/BoatActionEnum.java
@@ -8,6 +8,10 @@ import java.util.Map;
*/
public enum BoatActionEnum {
NOT_A_STATUS(-1),
+
+ /**
+ * Autopilot = auto VMG.
+ */
AUTO_PILOT(1),
SAILS_IN(2),
SAILS_OUT(3),
@@ -68,4 +72,4 @@ public enum BoatActionEnum {
}
}
-}
\ No newline at end of file
+}
diff --git a/racevisionGame/src/main/java/network/Messages/Enums/BoatLocationDeviceEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/BoatLocationDeviceEnum.java
new file mode 100644
index 00000000..42e8c733
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/Enums/BoatLocationDeviceEnum.java
@@ -0,0 +1,107 @@
+package network.Messages.Enums;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Various device sources for a BoatLocation message.
+ */
+public enum BoatLocationDeviceEnum {
+
+
+ NOT_A_DEVICE(-1),
+
+
+ Unknown(0),
+
+ /**
+ * A yacht particpating in the race.
+ */
+ RacingYacht(1),
+
+ CommitteeBoat(2),
+
+ /**
+ * A marker boat.
+ */
+ Mark(3),
+
+ Pin(4),
+
+ ChaseBoat(5),
+
+ MedicalBoat(6),
+
+ MarshallBoat(7),
+
+ UmpireBoat(8),
+
+ UmpireSoftwareApplication(9),
+
+ PrincipalRaceOfficerApplication(10),
+
+ WeatherStation(11),
+
+ Helicopter(12),
+
+ DataProcessingApplication(13);
+
+
+ /**
+ * Value of the enum.
+ */
+ private byte value;
+
+ /**
+ * Creates a BoatLocationDeviceEnum from a given primitive integer value, cast to a byte.
+ * @param value Integer, which is cast to byte, to construct from.
+ */
+ private BoatLocationDeviceEnum(int value) {
+ this.value = (byte) value;
+ }
+
+ /**
+ * Returns the primitive value of the enum.
+ * @return Primitive value of the enum.
+ */
+ public byte getValue() {
+ return value;
+ }
+
+
+ /**
+ * Stores a mapping between Byte values and BoatLocationDeviceEnum values.
+ */
+ private static final Map byteToDeviceMap = new HashMap<>();
+
+
+ /*
+ Static initialization block. Initializes the byteToDeviceMap.
+ */
+ static {
+ for (BoatLocationDeviceEnum type : BoatLocationDeviceEnum.values()) {
+ BoatLocationDeviceEnum.byteToDeviceMap.put(type.value, type);
+ }
+ }
+
+
+ /**
+ * Returns the enumeration value which corresponds to a given byte value.
+ * @param deviceValue Byte value to convert to a BoatLocationDeviceEnum value.
+ * @return The BoatLocationDeviceEnum value which corresponds to the given byte value.
+ */
+ public static BoatLocationDeviceEnum fromByte(byte deviceValue) {
+ //Gets the corresponding BoatLocationDeviceEnum from the map.
+ BoatLocationDeviceEnum type = BoatLocationDeviceEnum.byteToDeviceMap.get(deviceValue);
+
+ if (type == null) {
+ //If the byte value wasn't found, return the NOT_A_DEVICE BoatLocationDeviceEnum.
+ return BoatLocationDeviceEnum.NOT_A_DEVICE;
+ } else {
+ //Otherwise, return the BoatLocationDeviceEnum.
+ return type;
+ }
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/network/Messages/Enums/JoinAcceptanceEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/JoinAcceptanceEnum.java
new file mode 100644
index 00000000..908efa27
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/Enums/JoinAcceptanceEnum.java
@@ -0,0 +1,113 @@
+package network.Messages.Enums;
+
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This enum encapsulates the different ways in which a server may respond to a client {@link network.Messages.RequestToJoin} message.
+ */
+public enum JoinAcceptanceEnum {
+
+
+ /**
+ * Client is allowed to join and spectate.
+ */
+ JOIN_SUCCESSFUL_SPECTATOR(0),
+
+ /**
+ * Client is allowed to join and participate.
+ */
+ JOIN_SUCCESSFUL_PARTICIPANT(1),
+
+ /**
+ * Client is allowed to join and play the tutorial.
+ */
+ JOIN_SUCCESSFUL_TUTORIAL(2),
+
+ /**
+ * Client is allowed to join and participate as a ghost player.
+ */
+ JOIN_SUCCESSFUL_GHOST(3),
+
+
+ /**
+ * Join Request was denied.
+ */
+ JOIN_FAILURE(0x10),
+
+ /**
+ * The server is completely full, cannot participate or spectate.
+ */
+ SERVER_FULL(0x11),
+
+
+ /**
+ * Used to indicate that a given byte value is invalid.
+ */
+ NOT_AN_ACCEPTANCE_TYPE(-1);
+
+
+ /**
+ * Primitive value of the enum.
+ */
+ private byte value;
+
+
+ /**
+ * Ctor. Creates a JoinAcceptanceEnum from a given primitive integer value, cast to a byte.
+ * @param value Integer, which is cast to byte, to construct from.
+ */
+ private JoinAcceptanceEnum(int value) {
+ this.value = (byte) value;
+ }
+
+ /**
+ * Returns the primitive value of the enum.
+ * @return Primitive value of the enum.
+ */
+ public byte getValue() {
+ return value;
+ }
+
+
+
+
+ /**
+ * Stores a mapping between Byte values and JoinAcceptanceEnum values.
+ */
+ private static final Map byteToAcceptanceMap = new HashMap<>();
+
+
+ /*
+ Static initialization block. Initializes the byteToAcceptanceMap.
+ */
+ static {
+ for (JoinAcceptanceEnum type : JoinAcceptanceEnum.values()) {
+ JoinAcceptanceEnum.byteToAcceptanceMap.put(type.value, type);
+ }
+ }
+
+
+ /**
+ * Returns the enumeration value which corresponds to a given byte value.
+ * @param joinAcceptanceEnum Byte value to convert to a JoinAcceptanceEnum value.
+ * @return The RequestToJoinEnum value which corresponds to the given byte value.
+ */
+ public static JoinAcceptanceEnum fromByte(byte joinAcceptanceEnum) {
+ //Gets the corresponding MessageType from the map.
+ JoinAcceptanceEnum type = JoinAcceptanceEnum.byteToAcceptanceMap.get(joinAcceptanceEnum);
+
+ if (type == null) {
+ //If the byte value wasn't found, return the NOT_AN_ACCEPTANCE_TYPE JoinAcceptanceEnum.
+ return JoinAcceptanceEnum.NOT_AN_ACCEPTANCE_TYPE;
+ } else {
+ //Otherwise, return the JoinAcceptanceEnum.
+ return type;
+ }
+
+ }
+
+
+
+}
diff --git a/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingBoatStatusEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingBoatStatusEnum.java
new file mode 100644
index 00000000..692dba42
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingBoatStatusEnum.java
@@ -0,0 +1,88 @@
+package network.Messages.Enums;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Enumeration that encapsulates the various statuses a boat can have when rounding a mark.
+ */
+public enum MarkRoundingBoatStatusEnum {
+
+ UNKNOWN(0),
+
+ /**
+ * The boat is actively racing.
+ */
+ RACING(1),
+
+ /**
+ * The boat has been disqualified.
+ */
+ DSQ(2),
+
+ /**
+ * The boat has withdrawn from the race.
+ */
+ WITHDRAWN(3),
+
+ NOT_A_STATUS(-1);
+
+
+ /**
+ * Primitive value of the enum.
+ */
+ private byte value;
+
+
+ /**
+ * Ctor. Creates a {@link MarkRoundingBoatStatusEnum} from a given primitive integer value, cast to a byte.
+ * @param value Integer, which is cast to byte, to construct from.
+ */
+ private MarkRoundingBoatStatusEnum(int value) {
+ this.value = (byte)value;
+ }
+
+ /**
+ * Returns the primitive value of the enum.
+ * @return Primitive value of the enum.
+ */
+ public byte getValue() {
+ return value;
+ }
+
+
+ ///Stores a mapping between Byte values and MarkRoundingBoatStatusEnum values.
+ private static final Map byteToStatusMap = new HashMap<>();
+
+
+ /*
+ Static initialization block. Initializes the byteToStatusMap.
+ */
+ static {
+ for (MarkRoundingBoatStatusEnum type : MarkRoundingBoatStatusEnum.values()) {
+ byteToStatusMap.put(type.value, type);
+ }
+ }
+
+
+ /**
+ * Returns the enumeration value which corresponds to a given byte value.
+ * @param boatStatusByte Byte value to convert to a {@link MarkRoundingBoatStatusEnum} value.
+ * @return The {@link MarkRoundingBoatStatusEnum} value which corresponds to the given byte value.
+ */
+ public static MarkRoundingBoatStatusEnum fromByte(byte boatStatusByte) {
+ //Gets the corresponding MarkRoundingBoatStatusEnum from the map.
+ MarkRoundingBoatStatusEnum type = byteToStatusMap.get(boatStatusByte);
+
+ if (type == null) {
+ //If the byte value wasn't found, return the NOT_A_STATUS MarkRoundingBoatStatusEnum.
+ return MarkRoundingBoatStatusEnum.NOT_A_STATUS;
+ }
+ else {
+ //Otherwise, return the MarkRoundingBoatStatusEnum.
+ return type;
+ }
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingIDEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingIDEnum.java
new file mode 100644
index 00000000..69b0756c
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingIDEnum.java
@@ -0,0 +1,88 @@
+package network.Messages.Enums;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Enumeration that encapsulates the various mark identities.
+ */
+public enum MarkRoundingIDEnum {
+
+ UNKNOWN(0),
+
+
+ ENTRY_LIMIT_LINE(100),
+
+ ENTRY_LINE(101),
+
+ START_LINE(102),
+
+ FINISH_LINE(103),
+
+ SPEED_TEST_START(104),
+
+ SPEED_TEST_FINISH(105),
+
+ CLEAR_START(106),
+
+ NOT_AN_ID(-1);
+
+
+ /**
+ * Primitive value of the enum.
+ */
+ private byte value;
+
+
+ /**
+ * Ctor. Creates a {@link MarkRoundingIDEnum} from a given primitive integer value, cast to a byte.
+ * @param value Integer, which is cast to byte, to construct from.
+ */
+ private MarkRoundingIDEnum(int value) {
+ this.value = (byte)value;
+ }
+
+ /**
+ * Returns the primitive value of the enum.
+ * @return Primitive value of the enum.
+ */
+ public byte getValue() {
+ return value;
+ }
+
+
+ ///Stores a mapping between Byte values and MarkRoundingIDEnum values.
+ private static final Map byteToIDMap = new HashMap<>();
+
+
+ /*
+ Static initialization block. Initializes the byteToIDMap.
+ */
+ static {
+ for (MarkRoundingIDEnum type : MarkRoundingIDEnum.values()) {
+ byteToIDMap.put(type.value, type);
+ }
+ }
+
+
+ /**
+ * Returns the enumeration value which corresponds to a given byte value.
+ * @param sideByte Byte value to convert to a {@link MarkRoundingIDEnum} value.
+ * @return The {@link MarkRoundingIDEnum} value which corresponds to the given byte value.
+ */
+ public static MarkRoundingIDEnum fromByte(byte sideByte) {
+ //Gets the corresponding MarkRoundingIDEnum from the map.
+ MarkRoundingIDEnum type = byteToIDMap.get(sideByte);
+
+ if (type == null) {
+ //If the byte value wasn't found, return the NOT_AN_ID MarkRoundingIDEnum.
+ return MarkRoundingIDEnum.NOT_AN_ID;
+ }
+ else {
+ //Otherwise, return the MarkRoundingIDEnum.
+ return type;
+ }
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingSideEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingSideEnum.java
new file mode 100644
index 00000000..83aa75be
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingSideEnum.java
@@ -0,0 +1,82 @@
+package network.Messages.Enums;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Enumeration that encapsulates the various sides around which a boat may pass a mark.
+ */
+public enum MarkRoundingSideEnum {
+
+ UNKNOWN(0),
+
+ /**
+ * Boat rounded around port side of mark.
+ */
+ PORT(1),
+
+ /**
+ * Boat rounded around starboard side of mark.
+ */
+ STARBOARD(2),
+
+ NOT_A_SIDE(-1);
+
+ /**
+ * Primitive value of the enum.
+ */
+ private byte value;
+
+
+ /**
+ * Ctor. Creates a {@link MarkRoundingSideEnum} from a given primitive integer value, cast to a byte.
+ * @param value Integer, which is cast to byte, to construct from.
+ */
+ private MarkRoundingSideEnum(int value) {
+ this.value = (byte)value;
+ }
+
+ /**
+ * Returns the primitive value of the enum.
+ * @return Primitive value of the enum.
+ */
+ public byte getValue() {
+ return value;
+ }
+
+
+ ///Stores a mapping between Byte values and MarkRoundingSideEnum values.
+ private static final Map byteToSideMap = new HashMap<>();
+
+
+ /*
+ Static initialization block. Initializes the byteToSideMap.
+ */
+ static {
+ for (MarkRoundingSideEnum type : MarkRoundingSideEnum.values()) {
+ byteToSideMap.put(type.value, type);
+ }
+ }
+
+
+ /**
+ * Returns the enumeration value which corresponds to a given byte value.
+ * @param sideByte Byte value to convert to a {@link MarkRoundingSideEnum} value.
+ * @return The {@link MarkRoundingSideEnum} value which corresponds to the given byte value.
+ */
+ public static MarkRoundingSideEnum fromByte(byte sideByte) {
+ //Gets the corresponding MarkRoundingSideEnum from the map.
+ MarkRoundingSideEnum type = byteToSideMap.get(sideByte);
+
+ if (type == null) {
+ //If the byte value wasn't found, return the NOT_A_SIDE MarkRoundingSideEnum.
+ return MarkRoundingSideEnum.NOT_A_SIDE;
+ }
+ else {
+ //Otherwise, return the MarkRoundingSideEnum.
+ return type;
+ }
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingTypeEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingTypeEnum.java
new file mode 100644
index 00000000..559c17e9
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/Enums/MarkRoundingTypeEnum.java
@@ -0,0 +1,82 @@
+package network.Messages.Enums;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Enumeration that encapsulates the various types of marks that may be passed.
+ */
+public enum MarkRoundingTypeEnum {
+
+ UNKNOWN(0),
+
+ /**
+ * The mark is a singular mark.
+ */
+ MARK(1),
+
+ /**
+ * The mark is a gate (windward, leeward, start, finish, etc...).
+ */
+ GATE(2),
+
+ NOT_A_TYPE(-1);
+
+ /**
+ * Primitive value of the enum.
+ */
+ private byte value;
+
+
+ /**
+ * Ctor. Creates a {@link MarkRoundingTypeEnum} from a given primitive integer value, cast to a byte.
+ * @param value Integer, which is cast to byte, to construct from.
+ */
+ private MarkRoundingTypeEnum(int value) {
+ this.value = (byte)value;
+ }
+
+ /**
+ * Returns the primitive value of the enum.
+ * @return Primitive value of the enum.
+ */
+ public byte getValue() {
+ return value;
+ }
+
+
+ ///Stores a mapping between Byte values and MarkRoundingTypeEnum values.
+ private static final Map byteToTypeMap = new HashMap<>();
+
+
+ /*
+ Static initialization block. Initializes the byteToTypeMap.
+ */
+ static {
+ for (MarkRoundingTypeEnum type : MarkRoundingTypeEnum.values()) {
+ byteToTypeMap.put(type.value, type);
+ }
+ }
+
+
+ /**
+ * Returns the enumeration value which corresponds to a given byte value.
+ * @param sideByte Byte value to convert to a {@link MarkRoundingTypeEnum} value.
+ * @return The {@link MarkRoundingTypeEnum} value which corresponds to the given byte value.
+ */
+ public static MarkRoundingTypeEnum fromByte(byte sideByte) {
+ //Gets the corresponding MarkRoundingTypeEnum from the map.
+ MarkRoundingTypeEnum type = byteToTypeMap.get(sideByte);
+
+ if (type == null) {
+ //If the byte value wasn't found, return the NOT_A_TYPE MarkRoundingTypeEnum.
+ return MarkRoundingTypeEnum.NOT_A_TYPE;
+ }
+ else {
+ //Otherwise, return the MarkRoundingTypeEnum.
+ return type;
+ }
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java b/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java
index 086673f5..aed5d70a 100644
--- a/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java
+++ b/racevisionGame/src/main/java/network/Messages/Enums/MessageType.java
@@ -19,14 +19,37 @@ public enum MessageType {
MARKROUNDING(38),
COURSEWIND(44),
AVGWIND(47),
+
+
BOATACTION(100),
+
+ /**
+ * This is used for {@link network.Messages.RequestToJoin} messages.
+ */
+ REQUEST_TO_JOIN(101),
+
+ /**
+ * This is used for {@link network.Messages.JoinAcceptance} messages.
+ */
+ JOIN_ACCEPTANCE(102),
+
+
+ /**
+ * This is used for {@link network.Messages.AssignPlayerBoat} messages.
+ */
+ ASSIGN_PLAYER_BOAT(121),
+
NOTAMESSAGE(0);
- ///Primitive value of the enum.
+
+ /**
+ * Primitive value of the enum.
+ */
private byte value;
+
/**
- * Ctor. Creates a MessageType enum from a given primitive integer value, cast to a byte.
+ * Creates a MessageType enum from a given primitive integer value, cast to a byte.
* @param value Integer, which is cast to byte, to construct from.
*/
private MessageType(int value) {
diff --git a/racevisionGame/src/main/java/network/Messages/Enums/RaceStartTypeEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/RaceStartTypeEnum.java
new file mode 100644
index 00000000..d9b8d69b
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/Enums/RaceStartTypeEnum.java
@@ -0,0 +1,97 @@
+package network.Messages.Enums;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Enumeration that encapsulates the various types race start status notifications. See AC35 streaming spec, 4.5.
+ */
+public enum RaceStartTypeEnum {
+
+
+ /**
+ * The race start time is being set.
+ */
+ SET_RACE_START(1),
+
+ /**
+ * The race has been postponed.
+ */
+ RACE_POSTPONED(2),
+
+ /**
+ * The race has been abandoned.
+ */
+ RACE_ABANDONED(3),
+
+ /**
+ * The race has been terminated.
+ */
+ RACE_TERMINATED(4),
+
+ /**
+ * Used to indicate that a given byte value is invalid.
+ */
+ NOT_A_TYPE(-1);
+
+
+ /**
+ * Primitive value of the enum.
+ */
+ private byte value;
+
+
+ /**
+ * Ctor. Creates a RaceStartTypeEnum from a given primitive integer value, cast to a byte.
+ * @param value Integer, which is cast to byte, to construct from.
+ */
+ private RaceStartTypeEnum(int value) {
+ this.value = (byte) value;
+ }
+
+ /**
+ * Returns the primitive value of the enum.
+ * @return Primitive value of the enum.
+ */
+ public byte getValue() {
+ return value;
+ }
+
+
+ /**
+ * Stores a mapping between Byte values and RaceStartTypeEnum values.
+ */
+ private static final Map byteToTypeMap = new HashMap<>();
+
+
+ /*
+ Static initialization block. Initializes the byteToTypeMap.
+ */
+ static {
+ for (RaceStartTypeEnum type : RaceStartTypeEnum.values()) {
+ RaceStartTypeEnum.byteToTypeMap.put(type.value, type);
+ }
+ }
+
+
+ /**
+ * Returns the enumeration value which corresponds to a given byte value.
+ * @param startTypeEnum Byte value to convert to a RaceStartTypeEnum value.
+ * @return The RaceStartTypeEnum value which corresponds to the given byte value.
+ */
+ public static RaceStartTypeEnum fromByte(byte startTypeEnum) {
+ //Gets the corresponding MessageType from the map.
+ RaceStartTypeEnum type = RaceStartTypeEnum.byteToTypeMap.get(startTypeEnum);
+
+ if (type == null) {
+ //If the byte value wasn't found, return the NOT_A_TYPE RaceStartTypeEnum.
+ return RaceStartTypeEnum.NOT_A_TYPE;
+ } else {
+ //Otherwise, return the RaceStartTypeEnum.
+ return type;
+ }
+
+ }
+
+
+}
diff --git a/racevisionGame/src/main/java/network/Messages/Enums/RequestToJoinEnum.java b/racevisionGame/src/main/java/network/Messages/Enums/RequestToJoinEnum.java
new file mode 100644
index 00000000..d868a25d
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/Enums/RequestToJoinEnum.java
@@ -0,0 +1,102 @@
+package network.Messages.Enums;
+
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This enum encapsulates the different ways in which a client may wish to connect to a server.
+ */
+public enum RequestToJoinEnum {
+
+
+ /**
+ * Client wants to spectate.
+ */
+ SPECTATOR(0),
+
+ /**
+ * Client wants to participate.
+ */
+ PARTICIPANT(1),
+
+ /**
+ * Client wants to play the tutorial.
+ */
+ CONTROL_TUTORIAL(2),
+
+ /**
+ * Client wants to particpate as a ghost.
+ */
+ GHOST(3),
+
+
+ /**
+ * Used to indicate that a given byte value is invalid.
+ */
+ NOT_A_REQUEST_TYPE(-1);
+
+
+ /**
+ * Primitive value of the enum.
+ */
+ private int value;
+
+
+ /**
+ * Ctor. Creates a RequestToJoinEnum from a given int value.
+ * @param value Integer to construct from.
+ */
+ private RequestToJoinEnum(int value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the primitive value of the enum.
+ * @return Primitive value of the enum.
+ */
+ public int getValue() {
+ return value;
+ }
+
+
+
+
+ /**
+ * Stores a mapping between Integer values and RequestToJoinEnum values.
+ */
+ private static final Map intToRequestMap = new HashMap<>();
+
+
+ /*
+ Static initialization block. Initializes the intToRequestMap.
+ */
+ static {
+ for (RequestToJoinEnum type : RequestToJoinEnum.values()) {
+ RequestToJoinEnum.intToRequestMap.put(type.value, type);
+ }
+ }
+
+
+ /**
+ * Returns the enumeration value which corresponds to a given int value.
+ * @param requestToJoinEnum int value to convert to a RequestToJoinEnum value.
+ * @return The RequestToJoinEnum value which corresponds to the given int value.
+ */
+ public static RequestToJoinEnum fromInt(int requestToJoinEnum) {
+ //Gets the corresponding MessageType from the map.
+ RequestToJoinEnum type = RequestToJoinEnum.intToRequestMap.get(requestToJoinEnum);
+
+ if (type == null) {
+ //If the int value wasn't found, return the NOT_A_REQUEST_TYPE RequestToJoinEnum.
+ return RequestToJoinEnum.NOT_A_REQUEST_TYPE;
+ } else {
+ //Otherwise, return the RequestToJoinEnum.
+ return type;
+ }
+
+ }
+
+
+
+}
diff --git a/racevisionGame/src/main/java/network/Messages/Heartbeat.java b/racevisionGame/src/main/java/network/Messages/HeartBeat.java
similarity index 87%
rename from racevisionGame/src/main/java/network/Messages/Heartbeat.java
rename to racevisionGame/src/main/java/network/Messages/HeartBeat.java
index fb1dd23f..35f378ac 100644
--- a/racevisionGame/src/main/java/network/Messages/Heartbeat.java
+++ b/racevisionGame/src/main/java/network/Messages/HeartBeat.java
@@ -6,7 +6,7 @@ import network.Messages.Enums.MessageType;
/**
* Represents a Heartbeat message.
*/
-public class Heartbeat extends AC35Data {
+public class HeartBeat extends AC35Data {
/**
* Sequence number of the heartbeat.
@@ -17,7 +17,7 @@ public class Heartbeat extends AC35Data {
* Ctor.
* @param sequenceNumber Sequence number of the heartbeat.
*/
- public Heartbeat(long sequenceNumber) {
+ public HeartBeat(long sequenceNumber) {
super(MessageType.HEARTBEAT);
this.sequenceNumber = sequenceNumber;
}
diff --git a/racevisionGame/src/main/java/network/Messages/JoinAcceptance.java b/racevisionGame/src/main/java/network/Messages/JoinAcceptance.java
new file mode 100644
index 00000000..378d3285
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/JoinAcceptance.java
@@ -0,0 +1,54 @@
+package network.Messages;
+
+import network.Messages.Enums.JoinAcceptanceEnum;
+import network.Messages.Enums.MessageType;
+
+
+/**
+ * This is the message a server sends to a client to tell them their boat sourceID, and if they have actually managed to join the server.
+ */
+public class JoinAcceptance extends AC35Data {
+
+
+ /**
+ * The source ID of the boat assigned to the client.
+ * 0 indicates they haven't been assigned a boat.
+ */
+ private int sourceID = 0;
+
+ /**
+ * The type of acceptance response this is.
+ */
+ private JoinAcceptanceEnum acceptanceType;
+
+
+
+
+ /**
+ * Constructs a JoinAcceptance message of a given acceptance type.
+ * @param acceptanceType The type of join acceptance this is.
+ * @param sourceID The sourceID to assign to the client. 0 indicates no sourceID.
+ */
+ public JoinAcceptance(JoinAcceptanceEnum acceptanceType, int sourceID){
+ super(MessageType.JOIN_ACCEPTANCE);
+ this.acceptanceType = acceptanceType;
+ this.sourceID = sourceID;
+ }
+
+
+ /**
+ * The type of acceptance response this is.
+ * @return The type of acceptance response.
+ */
+ public JoinAcceptanceEnum getAcceptanceType() {
+ return acceptanceType;
+ }
+
+ /**
+ * Returns the source ID of the boat assigned to the client.
+ * @return The source ID of the boat assigned to the client.
+ */
+ public int getSourceID() {
+ return sourceID;
+ }
+}
diff --git a/racevisionGame/src/main/java/network/Messages/LatestMessages.java b/racevisionGame/src/main/java/network/Messages/LatestMessages.java
index f35fc52e..738b3ae6 100644
--- a/racevisionGame/src/main/java/network/Messages/LatestMessages.java
+++ b/racevisionGame/src/main/java/network/Messages/LatestMessages.java
@@ -1,11 +1,8 @@
package network.Messages;
import network.Messages.Enums.XMLMessageType;
-import shared.dataInput.RaceDataSource;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Observable;
+import java.util.*;
/**
* This class contains a set of the latest messages received (e.g., the latest RaceStatus, the latest BoatLocation for each boat, etc...).
@@ -13,35 +10,11 @@ import java.util.Observable;
*/
public class LatestMessages extends Observable {
- /**
- * The latest RaceStatus message.
- */
- private RaceStatus raceStatus;
-
- /**
- * A map of the last BoatStatus message received, for each boat.
- */
- private final Map boatStatusMap = new HashMap<>();
-
- /**
- * A map of the last BoatLocation message received, for each boat.
- */
- private final Map boatLocationMap = new HashMap<>();
-
- /**
- * A map of the last MarkRounding message received, for each boat.
- */
- private final Map markRoundingMap = new HashMap<>();
-
- /**
- * The last AverageWind message received.
- */
- private AverageWind averageWind;
/**
- * The last CourseWinds message received.
+ * A list of messages containing a snapshot of the race.
*/
- private CourseWinds courseWinds;
+ private List snapshot = new ArrayList<>();
/**
@@ -69,140 +42,25 @@ public class LatestMessages extends Observable {
}
-
-
- /**
- * Gets the latest RaceStatus message received.
- * @return The latest RaceStatus message received.
- */
- public RaceStatus getRaceStatus() {
- return raceStatus;
- }
-
- /**
- * Sets the latest RaceStatus message received.
- * @param raceStatus The new RaceStatus message to store.
- */
- public void setRaceStatus(RaceStatus raceStatus) {
- this.raceStatus = raceStatus;
- }
-
-
-
- /**
- * Returns the latest BoatStatus message received for a given boat.
- * @param sourceID Source ID of the boat.
- * @return The latest BoatStatus message for the specified boat.
- */
- public BoatStatus getBoatStatus(int sourceID) {
- return boatStatusMap.get(sourceID);
- }
-
- /**
- * Inserts a BoatStatus message for a given boat.
- * @param boatStatus The BoatStatus message to set.
- */
- public void setBoatStatus(BoatStatus boatStatus) {
- boatStatusMap.put(boatStatus.getSourceID(), boatStatus);
- }
-
-
-
- /**
- * Returns the latest BoatLocation message received for a given boat.
- * @param sourceID Source ID of the boat.
- * @return The latest BoatLocation message for the specified boat.
- */
- public BoatLocation getBoatLocation(int sourceID) {
- return boatLocationMap.get(sourceID);
- }
-
- /**
- * Inserts a BoatLocation message for a given boat.
- * @param boatLocation The BoatLocation message to set.
- */
- public void setBoatLocation(BoatLocation boatLocation) {
- //TODO should compare the sequence number of the new boatLocation with the existing boatLocation for this boat (if it exists), and use the newer one.
- boatLocationMap.put(boatLocation.getSourceID(), boatLocation);
- }
-
- /**
- * Returns the latest MarkRounding message received for a given boat.
- * @param sourceID Source ID of the boat.
- * @return The latest MarkRounding message for the specified boat.
- */
- public MarkRounding getMarkRounding(int sourceID) {
- return markRoundingMap.get(sourceID);
- }
-
- /**
- * Inserts a MarkRounding message for a given boat.
- * @param markRounding The MarkRounding message to set.
- */
- public void setMarkRounding(MarkRounding markRounding) {
- //TODO should compare the sequence number of the new markRounding with the existing boatLocation for this boat (if it exists), and use the newer one.
- markRoundingMap.put(markRounding.getSourceID(), markRounding);
- }
-
-
-
/**
- * Gets the latest AverageWind message received.
- * @return The latest AverageWind message received.
+ * Returns a copy of the race snapshot.
+ * @return Copy of the race snapshot.
*/
- public AverageWind getAverageWind() {
- return averageWind;
- }
-
- /**
- * Sets the latest AverageWind message received.
- * @param averageWind The new AverageWind message to store.
- */
- public void setAverageWind(AverageWind averageWind) {
- this.averageWind = averageWind;
+ public List getSnapshot() {
+ return new ArrayList<>(snapshot);
}
/**
- * Gets the latest CourseWinds message received.
- * @return The latest CourseWinds message received.
+ * Sets the snapshot of the race.
+ * @param snapshot New snapshot of race.
*/
- public CourseWinds getCourseWinds() {
- return courseWinds;
+ public void setSnapshot(List snapshot) {
+ this.snapshot = snapshot;
}
- /**
- * Sets the latest CourseWinds message received.
- * @param courseWinds The new CourseWinds message to store.
- */
- public void setCourseWinds(CourseWinds courseWinds) {
- this.courseWinds = courseWinds;
- }
-
-
- /**
- * Returns the map of boat sourceIDs to BoatLocation messages.
- * @return Map between boat sourceID and BoatLocation.
- */
- public Map getBoatLocationMap() {
- return boatLocationMap;
- }
- /**
- * Returns the map of boat sourceIDs to BoatStatus messages.
- * @return Map between boat sourceID and BoatStatus.
- */
- public Map getBoatStatusMap() {
- return boatStatusMap;
- }
- /**
- * Returns the map of boat sourceIDs to MarkRounding messages.
- * @return Map between boat sourceID and MarkRounding.
- */
- public Map getMarkRoundingMap() {
- return markRoundingMap;
- }
diff --git a/racevisionGame/src/main/java/network/Messages/MarkRounding.java b/racevisionGame/src/main/java/network/Messages/MarkRounding.java
index a13f0ba7..eae2daed 100644
--- a/racevisionGame/src/main/java/network/Messages/MarkRounding.java
+++ b/racevisionGame/src/main/java/network/Messages/MarkRounding.java
@@ -1,47 +1,94 @@
package network.Messages;
+import network.Messages.Enums.MarkRoundingBoatStatusEnum;
+import network.Messages.Enums.MarkRoundingSideEnum;
+import network.Messages.Enums.MarkRoundingTypeEnum;
import network.Messages.Enums.MessageType;
/**
- * Created by fwy13 on 25/04/17.
+ * Represents a MarkRound message (see AC35 spec, 4.10).
*/
public class MarkRounding extends AC35Data {
- private int msgVerNum;
+ /**
+ * The current messageVersionNumber according to the API spec.
+ */
+ public static final byte currentMessageVersionNumber = 1;
+
+
+ /**
+ * Version number of the message.
+ */
+ private byte messageVersionNumber;
+
+ /**
+ * The time at which the mark was rounding. Milliseconds since unix epoch.
+ */
private long time;
+
+ /**
+ * The ack number of the message.
+ */
private int ackNum;
+
+ /**
+ * The raceID this message relates to.
+ */
private int raceID;
+
+ /**
+ * The sourceID of the boat this message relates to.
+ */
private int sourceID;
- private int boatStatus;
- private int roundingSide;
- private int markType;
- private int markID;
-
- public static int BoatStatusUnknown = 0;
- public static int BoatStatusRacing = 1;
- public static int BoatStatusDSQ = 2;
- public static int BoatStatusWithdrawn = 3;
-
- public static int RoundingSideUnknown = 0;
- public static int RoundingSidePort = 1;
- public static int RoundingSideStarboard = 2;
-
- public static int MarkTypeUnknown = 0;
- public static int MarkTypeRoundingMark = 1;
- public static int MarkTypeGate = 2;
-
- public static int MarkIDEntryLimitLine = 100;
- public static int MarkIDEntryLine = 101;
- public static int MarkIDRaceStartStartline = 102;
- public static int MarkIDRaceFinishline = 103;
- public static int MarkIDSpeedTestStart = 104;
- public static int MarkIDSpeedTestFinish = 105;
- public static int MarkIDClearStart = 106;
-
- public MarkRounding(int msgVerNum, long time, int ackNum, int raceID, int sourceID, int boatStatus, int roundingSide, int markType, int markID){
+
+ /**
+ * The status of the boat.
+ */
+ private MarkRoundingBoatStatusEnum boatStatus;
+
+ /**
+ * The side around which the boat rounded.
+ */
+ private MarkRoundingSideEnum roundingSide;
+
+ /**
+ * The type of mark that was rounded.
+ */
+ private MarkRoundingTypeEnum markType;
+
+ /**
+ * The ID of the mark. This is not a source ID.
+ */
+ private byte markID;
+
+
+ /**
+ * Creates a MarkRounding message with the given parameters.
+ * @param messageVersionNumber The version number of the message.
+ * @param time The time at which the message was created.
+ * @param ackNum The ack number of the message.
+ * @param raceID The raceID this message relates to.
+ * @param sourceID The sourceID of the boat this message relates to.
+ * @param boatStatus The status of the boat as it rounded the mark.
+ * @param roundingSide The side around which the boat rounded.
+ * @param markType The type of mark that was rounded.
+ * @param markID The ID number of the mark. Not a sourceID. See {@link network.Messages.Enums.MarkRoundingIDEnum}.
+ */
+ public MarkRounding(
+ byte messageVersionNumber,
+ long time,
+ int ackNum,
+ int raceID,
+ int sourceID,
+ MarkRoundingBoatStatusEnum boatStatus,
+ MarkRoundingSideEnum roundingSide,
+ MarkRoundingTypeEnum markType,
+ byte markID ) {
+
super(MessageType.MARKROUNDING);
- this.msgVerNum = msgVerNum;
+
+ this.messageVersionNumber = messageVersionNumber;
this.time = time;
this.ackNum = ackNum;
this.raceID = raceID;
@@ -52,6 +99,40 @@ public class MarkRounding extends AC35Data {
this.markID = markID;
}
+
+ /**
+ * Returns the version number of this message.
+ * @return Version number of this message.
+ */
+ public byte getMessageVersionNumber() {
+ return messageVersionNumber;
+ }
+
+
+ /**
+ * Returns the timestamp for this message.
+ * @return Timestamp for this message.
+ */
+ public long getTime() {
+ return time;
+ }
+
+ /**
+ * Returns the ack number of this message.
+ * @return Ack number of this message.
+ */
+ public int getAckNum() {
+ return ackNum;
+ }
+
+ /**
+ * Returns the raceID this message relates to.
+ * @return RaceID this message relates to.
+ */
+ public int getRaceID() {
+ return raceID;
+ }
+
/**
* Returns the boat (source) ID for this message.
* @return Boat ID for this message.
@@ -61,10 +142,34 @@ public class MarkRounding extends AC35Data {
}
/**
- * Returns the timestamp for this message.
- * @return Timestamp for this message.
+ * Returns the status of the boat as it rounded the mark.
+ * @return Status of boat as it rounded mark.
*/
- public long getTime() {
- return time;
+ public MarkRoundingBoatStatusEnum getBoatStatus() {
+ return boatStatus;
+ }
+
+ /**
+ * Returns the side to which the boat rounded the mark.
+ * @return Side to which boat rounded mark.
+ */
+ public MarkRoundingSideEnum getRoundingSide() {
+ return roundingSide;
+ }
+
+ /**
+ * Returns the type of mark that was rounded.
+ * @return The type of mark that was rounded.
+ */
+ public MarkRoundingTypeEnum getMarkType() {
+ return markType;
+ }
+
+ /**
+ * Returns ID number of the mark. This is not a source ID. See {@link network.Messages.Enums.MarkRoundingIDEnum}.
+ * @return ID number of the mark.
+ */
+ public byte getMarkID() {
+ return markID;
}
}
diff --git a/racevisionGame/src/main/java/network/Messages/RaceSnapshot.java b/racevisionGame/src/main/java/network/Messages/RaceSnapshot.java
new file mode 100644
index 00000000..212c8dab
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/RaceSnapshot.java
@@ -0,0 +1,41 @@
+package network.Messages;
+
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Represents a snapshot of the race's state.
+ * Contains a list of {@link AC35Data} messages.
+ * Send a copy of each message to a connected client.
+ */
+public class RaceSnapshot {
+
+ /**
+ * The contents of the snapshot.
+ */
+ private List snapshot;
+
+
+ /**
+ * Constructs a snapshot using a given list of messages.
+ * @param snapshot Messages to use as snapshot.
+ */
+ public RaceSnapshot(List snapshot) {
+ this.snapshot = snapshot;
+ }
+
+
+ /**
+ * Gets the contents of the snapshot.
+ * This is a shallow copy.
+ * @return Contents of the snapshot.
+ */
+ public List getSnapshot() {
+
+ List copy = new ArrayList<>(snapshot);
+
+ return copy;
+ }
+}
diff --git a/racevisionGame/src/main/java/network/Messages/RaceStartStatus.java b/racevisionGame/src/main/java/network/Messages/RaceStartStatus.java
index 8dc442ab..6ad514d5 100644
--- a/racevisionGame/src/main/java/network/Messages/RaceStartStatus.java
+++ b/racevisionGame/src/main/java/network/Messages/RaceStartStatus.java
@@ -2,20 +2,63 @@ package network.Messages;
import network.Messages.Enums.MessageType;
+import network.Messages.Enums.RaceStartTypeEnum;
+
/**
- * Created by fwy13 on 25/04/17.
+ * Represents a RaceStartStatus message from the API, section 4.5.
*/
public class RaceStartStatus extends AC35Data {
+ /**
+ * The current version number of this message type.
+ */
+ public static final byte currentMessageVersionNumber = 1;
+
+
+ /**
+ * The version number of this message.
+ */
+ private byte messageVersionNumber;
+
+ /**
+ * The time at which this message was created. Milliseconds since unix epoch.
+ */
private long timestamp;
+
+ /**
+ * Sequence number of message.
+ */
private int ackNum;
+
+ /**
+ * The time the race is expected to start at. Milliseconds since unix epoch.
+ */
private long raceStartTime;
+
+ /**
+ * The ID of the race this message relates to.
+ */
private int raceID;
- private int notificationType;
- public RaceStartStatus(long timestamp, int ackNum, long raceStartTime, int raceID, int notificationType){
+ /**
+ * The type of notification this is.
+ */
+ private RaceStartTypeEnum notificationType;
+
+
+ /**
+ * Constructs a RaceStartStatus message with the given parameters.
+ * @param messageVersionNumber Version number of the message.
+ * @param timestamp The timestamp at which this message was generated.
+ * @param ackNum The sequence number of this message.
+ * @param raceStartTime The expected race start time.
+ * @param raceID The ID of the race this message relates to.
+ * @param notificationType The type of notification this is.
+ */
+ public RaceStartStatus(byte messageVersionNumber, long timestamp, int ackNum, long raceStartTime, int raceID, RaceStartTypeEnum notificationType) {
super(MessageType.RACESTARTSTATUS);
+ this.messageVersionNumber = messageVersionNumber;
this.timestamp = timestamp;
this.ackNum = ackNum;
this.raceStartTime = raceStartTime;
@@ -23,4 +66,52 @@ public class RaceStartStatus extends AC35Data {
this.notificationType = notificationType;
}
+
+ /**
+ * Returns the version number of this message.
+ * @return Version number of this message.
+ */
+ public byte getMessageVersionNumber() {
+ return messageVersionNumber;
+ }
+
+ /**
+ * Return the time at which this message was generated. Milliseconds since unix epoch.
+ * @return Time at which this message was generated.
+ */
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ /**
+ * Returns the sequence number of this message.
+ * @return Sequence number of this message.
+ */
+ public int getAckNum() {
+ return ackNum;
+ }
+
+ /**
+ * Returns the expected race start time. Milliseconds since unix epoch.
+ * @return Expected race start time.
+ */
+ public long getRaceStartTime() {
+ return raceStartTime;
+ }
+
+ /**
+ * Returns the race ID this message relates to.
+ * @return Race ID this message relates to.
+ */
+ public int getRaceID() {
+ return raceID;
+ }
+
+ /**
+ * Returns the type of start status notification this message is.
+ * @return The type of notification this is.
+ */
+ public RaceStartTypeEnum getNotificationType() {
+ return notificationType;
+ }
}
diff --git a/racevisionGame/src/main/java/network/Messages/RaceStatus.java b/racevisionGame/src/main/java/network/Messages/RaceStatus.java
index 42bb8208..574cfce4 100644
--- a/racevisionGame/src/main/java/network/Messages/RaceStatus.java
+++ b/racevisionGame/src/main/java/network/Messages/RaceStatus.java
@@ -2,27 +2,92 @@ package network.Messages;
import network.Messages.Enums.MessageType;
+import network.Messages.Enums.RaceStatusEnum;
+import network.Messages.Enums.RaceTypeEnum;
import network.Utils.AC35UnitConverter;
+import shared.model.Bearing;
import shared.model.Constants;
import java.util.List;
/**
- * Created by fwy13 on 25/04/17.
+ * Represents the information in a RaceStatus message (AC streaming spec: 4.2).
*/
public class RaceStatus extends AC35Data {
+
+ /**
+ * The current messageVersionNumber according to the API spec.
+ */
+ public static final byte currentMessageVersionNumber = 2;
+
+
+ /**
+ * Version number of the message.
+ */
+ private byte messageVersionNumber;
+
+ /**
+ * Time the message was generated at.
+ * Milliseconds since unix epoch.
+ */
private long currentTime;
+
+ /**
+ * ID number of the race.
+ */
private int raceID;
- private byte raceStatus;
+
+ /**
+ * The status of the race.
+ */
+ private RaceStatusEnum raceStatus;
+
+ /**
+ * The expected race start time.
+ * Milliseconds since unix epoch.
+ */
private long expectedStartTime;
- private int windDirection;
- private int windSpeed;
- private int raceType;
+
+ /**
+ * The wind direction of the course.
+ */
+ private Bearing windDirection;
+
+ /**
+ * The wind speed of the course.
+ * Knots.
+ */
+ private double windSpeed;
+
+ /**
+ * The type of race this is.
+ */
+ private RaceTypeEnum raceType;
+
+ /**
+ * A list of boat statuses.
+ * One for each boat.
+ */
private List boatStatuses;
- public RaceStatus(long currentTime, int raceID, byte raceStatus, long expectedStartTime, int windDirection, int windSpeed, int raceType, List boatStatuses){
+
+ /**
+ * Constructs a RaceStatus message with the given parameters.
+ * @param messageVersionNumber The version number of the message.
+ * @param currentTime Time at which the message was generated.
+ * @param raceID The ID of the race.
+ * @param raceStatus The status of the race.
+ * @param expectedStartTime The expected start time of the race.
+ * @param windDirection The current wind direction in the race.
+ * @param windSpeed The current wind speed in the race, in knots.
+ * @param raceType The type of race this is.
+ * @param boatStatuses A list of BoatStatuses. One for each boat.
+ */
+ public RaceStatus(byte messageVersionNumber, long currentTime, int raceID, RaceStatusEnum raceStatus, long expectedStartTime, Bearing windDirection, double windSpeed, RaceTypeEnum raceType, List boatStatuses) {
+
super(MessageType.RACESTATUS);
+ this.messageVersionNumber = messageVersionNumber;
this.currentTime = currentTime;
this.raceID = raceID;
this.raceStatus = raceStatus;
@@ -30,114 +95,134 @@ public class RaceStatus extends AC35Data {
this.windDirection = windDirection;
this.windSpeed = windSpeed;
this.raceType = raceType;
- this.boatStatuses = boatStatuses;//note this is not a copy so any alterations to the parent will affect this.
+ this.boatStatuses = boatStatuses;
}
- ///Getters.
+ /**
+ * Returns the version number of this message.
+ * @return The version number of the message.
+ */
+ public byte getMessageVersionNumber() {
+ return messageVersionNumber;
+ }
+ /**
+ * Returns the current time at which this message was generated. Milliseconds since unix epoch.
+ * @return Time this message was generated at.
+ */
public long getCurrentTime()
{
return currentTime;
}
+ /**
+ * Returns the RaceID.
+ * @return The raceID.
+ */
public int getRaceID()
{
return raceID;
}
/**
- *
- * @return race status number
+ * Returns the race status.
+ * @return The current race status.
*/
- public byte getRaceStatus()
+ public RaceStatusEnum getRaceStatus()
{
return raceStatus;
}
+ /**
+ * Returns the expected start time for the race. Milliseconds since unix epoch.
+ * @return Expected start time for the race.
+ */
public long getExpectedStartTime()
{
return expectedStartTime;
}
- public int getWindDirection()
+ /**
+ * Returns the current direction of the wind in the race.
+ * @return Current wind direction.
+ */
+ public Bearing getWindDirection()
{
return windDirection;
}
/**
- * Returns the wind speed for this race status, in millimeters per second.
- * @return Wind speed in millimeters per second.
+ * Returns the wind speed for this race status, in knots.
+ * @return Wind speed in knots.
*/
- public int getWindSpeed()
+ public double getWindSpeed()
{
return windSpeed;
}
- public int getRaceType()
+ /**
+ * Retrusn the type of race this is.
+ * @return The type of race this is.
+ */
+ public RaceTypeEnum getRaceType()
{
return raceType;
}
+ /**
+ * Returns the list of BoatStatuses. One for each boat.
+ * @return List of BoatStatuses.
+ */
public List getBoatStatuses()
{
return boatStatuses;
}
+
public boolean isNotActive() {
- return raceStatus == 0;
+ return raceStatus == RaceStatusEnum.NOT_ACTIVE;
}
public boolean isWarning() {
- return raceStatus == 1;
+ return raceStatus == RaceStatusEnum.WARNING;
}
public boolean isPreparatory() {
- return raceStatus == 2;
+ return raceStatus == RaceStatusEnum.PREPARATORY;
}
public boolean isStarted() {
- return raceStatus == 3;
+ return raceStatus == RaceStatusEnum.STARTED;
}
public boolean isFinished() {
- return raceStatus == 4;
+ return raceStatus == RaceStatusEnum.FINISHED;
}
public boolean isRetired() {
- return raceStatus == 5;
+ return raceStatus == RaceStatusEnum.RETIRED;
}
public boolean isAbandoned() {
- return raceStatus == 6;
+ return raceStatus == RaceStatusEnum.ABANDONED;
}
public boolean isPostponed() {
- return raceStatus == 7;
+ return raceStatus == RaceStatusEnum.POSTPONED;
}
public boolean isTerminated() {
- return raceStatus == 8;
+ return raceStatus == RaceStatusEnum.TERMINATED;
}
public boolean isStartTimeSet() {
- return raceStatus != 9;
+ return raceStatus != RaceStatusEnum.RACE_START_TIME_NOT_SET;
}
public boolean isPrestart() {
- return raceStatus == 10;
+ return raceStatus == RaceStatusEnum.PRESTART;
}
- public double getScaledWindDirection() {
- return AC35UnitConverter.convertHeading(windDirection);
- }
-
- /**
- * Returns the wind speed for this race status, in knots.
- * @return Wind speed in knots.
- */
- public double getWindSpeedKnots() {
- return (windSpeed / Constants.KnotsToMMPerSecond);
- }
}
diff --git a/racevisionGame/src/main/java/network/Messages/RequestToJoin.java b/racevisionGame/src/main/java/network/Messages/RequestToJoin.java
new file mode 100644
index 00000000..291bb138
--- /dev/null
+++ b/racevisionGame/src/main/java/network/Messages/RequestToJoin.java
@@ -0,0 +1,36 @@
+package network.Messages;
+
+
+import network.Messages.Enums.MessageType;
+import network.Messages.Enums.RequestToJoinEnum;
+
+/**
+ * This is the message a client sends to a server to request to join/view a race.
+ */
+public class RequestToJoin extends AC35Data {
+
+
+ /**
+ * The type of join request this is.
+ */
+ private RequestToJoinEnum requestType;
+
+
+ /**
+ * Constructs a RequestToJoin message of a given request type.
+ * @param requestType The type of join request this is.
+ */
+ public RequestToJoin(RequestToJoinEnum requestType){
+ super(MessageType.REQUEST_TO_JOIN);
+ this.requestType = requestType;
+ }
+
+
+ /**
+ * The type of join request this is.
+ * @return The type of join request.
+ */
+ public RequestToJoinEnum getRequestType() {
+ return requestType;
+ }
+}
diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java
new file mode 100644
index 00000000..2dbbe4e0
--- /dev/null
+++ b/racevisionGame/src/main/java/network/StreamRelated/MessageDeserialiser.java
@@ -0,0 +1,152 @@
+package network.StreamRelated;
+
+
+import network.BinaryMessageDecoder;
+import network.Exceptions.InvalidMessageException;
+import network.Messages.AC35Data;
+import shared.model.RunnableWithFramePeriod;
+
+import java.io.*;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static network.Utils.ByteConverter.bytesToShort;
+
+/**
+ * This class is responsible for converting data from an input stream into a queue of {@link AC35Data} messages.
+ */
+public class MessageDeserialiser implements RunnableWithFramePeriod {
+
+
+ /**
+ * The stream we're reading from.
+ */
+ private DataInputStream inputStream;
+
+ /**
+ * The messages we've read.
+ */
+ private BlockingQueue messagesRead;
+
+
+ /**
+ * Determines whether or not this runnable is currently running.
+ */
+ private boolean isRunning;
+
+
+
+
+ /**
+ * Constructs a new MessageSerialiser to write a queue of messages to a given stream.
+ * @param inputStream The stream to write to.
+ * @param messagesRead The messages to send.
+ */
+ public MessageDeserialiser(InputStream inputStream, BlockingQueue messagesRead) {
+ this.inputStream = new DataInputStream(inputStream);
+ this.messagesRead = messagesRead;
+ }
+
+ /**
+ * Returns the queue of messages read from the socket.
+ * @return Queue of messages read from socket.
+ */
+ public BlockingQueue getMessagesRead() {
+ return messagesRead;
+ }
+
+
+ /**
+ * Reads and returns the next message as an array of bytes from the input stream. Use getNextMessage() to get the actual message object instead.
+ * @return Encoded binary message bytes.
+ * @throws IOException Thrown when an error occurs while reading from the input stream.
+ */
+ private byte[] getNextMessageBytes() throws IOException {
+ inputStream.mark(0);
+ short CRCLength = 4;
+ short headerLength = 15;
+
+ //Read the header of the next message.
+ byte[] headerBytes = new byte[headerLength];
+ inputStream.readFully(headerBytes);
+
+ //Read the message body length.
+ byte[] messageBodyLengthBytes = Arrays.copyOfRange(headerBytes, headerLength - 2, headerLength);
+ short messageBodyLength = bytesToShort(messageBodyLengthBytes);
+
+ //Read the message body.
+ byte[] messageBodyBytes = new byte[messageBodyLength];
+ inputStream.readFully(messageBodyBytes);
+
+ //Read the message CRC.
+ byte[] messageCRCBytes = new byte[CRCLength];
+ inputStream.readFully(messageCRCBytes);
+
+ //Put the head + body + crc into one large array.
+ ByteBuffer messageBytes = ByteBuffer.allocate(headerBytes.length + messageBodyBytes.length + messageCRCBytes.length);
+ messageBytes.put(headerBytes);
+ messageBytes.put(messageBodyBytes);
+ messageBytes.put(messageCRCBytes);
+
+ return messageBytes.array();
+ }
+
+
+ /**
+ * Reads and returns the next message object from the input stream.
+ * @return The message object.
+ * @throws IOException Thrown when an error occurs while reading from the input stream.
+ * @throws InvalidMessageException Thrown when the message is invalid in some way.
+ */
+ private AC35Data getNextMessage() throws IOException, InvalidMessageException
+ {
+ //Get the next message from the socket as a block of bytes.
+ byte[] messageBytes = this.getNextMessageBytes();
+
+ //Decode the binary message into an appropriate message object.
+ BinaryMessageDecoder decoder = new BinaryMessageDecoder(messageBytes);
+
+ return decoder.decode();
+
+ }
+
+
+ /**
+ * Determines whether or not this runnable is running.
+ * @return True means that it is still running, false means that it has stopped.
+ */
+ public boolean isRunning() {
+ return isRunning;
+ }
+
+
+
+ @Override
+ public void run() {
+
+ isRunning = true;
+
+ while (!Thread.interrupted()) {
+
+ //Reads the next message.
+ try {
+ AC35Data message = this.getNextMessage();
+ messagesRead.add(message);
+ }
+ catch (InvalidMessageException e) {
+ Logger.getGlobal().log(Level.WARNING, "Unable to read message on thread: " + Thread.currentThread() + ".", e);
+
+ } catch (IOException e) {
+ Logger.getGlobal().log(Level.SEVERE, "Unable to read inputStream: " + inputStream + " on thread: " + Thread.currentThread() + ".", e);
+ isRunning = false;
+ return;
+
+ }
+
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java
new file mode 100644
index 00000000..84bddf78
--- /dev/null
+++ b/racevisionGame/src/main/java/network/StreamRelated/MessageSerialiser.java
@@ -0,0 +1,125 @@
+package network.StreamRelated;
+
+
+import network.Exceptions.InvalidMessageException;
+import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Messages.AC35Data;
+import shared.model.RunnableWithFramePeriod;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class is responsible for writing a queue of {@link network.Messages.AC35Data} messages to an output stream.
+ */
+public class MessageSerialiser implements RunnableWithFramePeriod {
+
+
+ /**
+ * The stream we're writing to.
+ */
+ private DataOutputStream outputStream;
+
+ /**
+ * The messages we're writing to the stream.
+ */
+ private BlockingQueue messagesToSend;
+
+
+ /**
+ * Ack numbers used in messages.
+ */
+ private int ackNumber = 1;
+
+ /**
+ * Determines whether or not this runnable is currently running.
+ */
+ private boolean isRunning;
+
+
+
+ /**
+ * Constructs a new MessageSerialiser to write a queue of messages to a given stream.
+ * @param outputStream The stream to write to.
+ * @param messagesToSend The messages to send.
+ */
+ public MessageSerialiser(OutputStream outputStream, BlockingQueue messagesToSend) {
+ this.outputStream = new DataOutputStream(outputStream);
+ this.messagesToSend = messagesToSend;
+ }
+
+ /**
+ * Returns the queue of messages to write to the socket.
+ * @return Queue of messages to write to the socket.
+ */
+ public BlockingQueue getMessagesToSend() {
+ return messagesToSend;
+ }
+
+
+ /**
+ * Increments the ackNumber value, and returns it.
+ * @return Incremented ackNumber.
+ */
+ private int getNextAckNumber(){
+ this.ackNumber++;
+
+ return this.ackNumber;
+ }
+
+ /**
+ * Determines whether or not this runnable is running.
+ * @return True means that it is still running, false means that it has stopped.
+ */
+ public boolean isRunning() {
+ return isRunning;
+ }
+
+
+ @Override
+ public void run() {
+
+ long previousFrameTime = System.currentTimeMillis();
+
+ isRunning = true;
+
+ while (!Thread.interrupted()) {
+
+
+ long currentFrameTime = System.currentTimeMillis();
+ waitForFramePeriod(previousFrameTime, currentFrameTime, 16);
+ previousFrameTime = currentFrameTime;
+
+
+ //Send the messages.
+ List messages = new ArrayList<>();
+ messagesToSend.drainTo(messages);
+
+ for (AC35Data message : messages) {
+ try {
+ byte[] messageBytes = RaceVisionByteEncoder.encodeBinaryMessage(message, getNextAckNumber());
+
+ outputStream.write(messageBytes);
+
+
+ } catch (InvalidMessageException e) {
+ Logger.getGlobal().log(Level.WARNING, "Could not encode message: " + message, e);
+
+ } catch (IOException e) {
+ Logger.getGlobal().log(Level.SEVERE, "Could not write message to outputStream: " + outputStream + " on thread: " + Thread.currentThread(), e);
+ isRunning = false;
+ return;
+
+ }
+ }
+
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/network/Utils/AC35UnitConverter.java b/racevisionGame/src/main/java/network/Utils/AC35UnitConverter.java
index 73f6d0e9..40b87628 100644
--- a/racevisionGame/src/main/java/network/Utils/AC35UnitConverter.java
+++ b/racevisionGame/src/main/java/network/Utils/AC35UnitConverter.java
@@ -1,43 +1,107 @@
package network.Utils;
+import shared.model.Constants;
+
/**
- * Created by fwy13 on 28/04/17.
+ * Contains various unit conversion for encoding/decoding messages.
+ * Our program uses the "unpacked" units, and the over-the-wire format uses "packed" units (e.g., degrees stored as ints).
*/
public class AC35UnitConverter {
- public static double convertGPS(int value){
- //converts latitude or longitue to angle
+
+ /**
+ * Converts a packed GPSCoordinate (latitude or longitude) into the unpacked unit.
+ * @param value Packed lat/long value.
+ * @return Unpacked lat/long angle, in degrees.
+ */
+ public static double unpackGPS(int value) {
return (double) value * 180.0 / 2147483648.0;//2^31 = 2147483648
}
- public static int convertGPSToInt(double value){
- //converts latitude or longitue to angle
- return (int) (value * 2147483648.0/180.0);//2^31 = 2147483648
+ /**
+ * Converts a latitude or longitude angle into a packed unit.
+ * @param value The lat/long angle, in degrees, to convert.
+ * @return The packed value.
+ */
+ public static int packGPS(double value) {
+ return (int) (value * 2147483648.0 / 180.0);//2^31 = 2147483648
}
- public static double convertHeading(long value){
- return (double) value * 360.0/65536.0;//2^15
+
+ /**
+ * Unpacks a heading from an int to an angle in degrees (this is a bearing).
+ * @param value The packed value to unpack.
+ * @return The unpacked value in degrees.
+ */
+ public static double unpackHeading(int value) {
+ return (value * 360.0 / 65536.0);//2^15
}
- public static double convertHeading(int value){
- return (double) value * 360.0/65536.0;//2^15
+ /**
+ * Packs a heading (this is a bearing), in degrees, to a packed int value.
+ * @param value The heading in degrees.
+ * @return The packed value.
+ */
+ public static int packHeading(double value) {
+ return (int) (value / 360.0 * 65536.0);//2^15
}
- public static double convertHeading(double value){
- return value * 360.0/65536.0;//2^15
+ /**
+ * Unpacks a true wind angle from a short to an angle in degrees (this is an azimuth).
+ * @param value The packed value to unpack.
+ * @return The unpacked value in degrees.
+ */
+ public static double unpackTrueWindAngle(short value) {
+ return (value * 180.0 / 32768.0);//-2^15 to 2^15
}
- public static int encodeHeading(int value){
- return (int) (value / 360.0 * 65536.0);//2^15
+ /**
+ * Packs a true wind angle (this is an azimuth) from an angle in degrees to a packed short value.
+ * @param value The unpacked value in degrees.
+ * @return The packed value.
+ */
+ public static short packTrueWindAngle(double value) {
+ return (short) (value / 180.0 * 32768.0);//-2^15 to 2^15
}
- public static int encodeHeading(double value){
- return (int) (value / 360.0 * 65536.0);//2^15
+
+ /**
+ * Unpacks a speed, in millimeters per second, to a double, in knots.
+ * @param millimetersPerSec Speed in millimeters per second.
+ * @return Speed in knots.
+ */
+ public static double unpackMMperSecToKnots(int millimetersPerSec) {
+ return (millimetersPerSec / Constants.KnotsToMMPerSecond);
}
- public static double convertTrueWindAngle(long value){
- return (double) value * 180.0/32768.0;//-2^15 to 2^15
+ /**
+ * Packs a speed, in knots, into an int, in millimeters per second.
+ * @param speedKnots Speed in knots.
+ * @return Speed in millimeters per second.
+ */
+ public static int packKnotsToMMperSec(double speedKnots) {
+ return (int) (speedKnots * Constants.KnotsToMMPerSecond);
}
+
+ /**
+ * Packs an average wind period, in milliseconds, into an int, in tenths of a second.
+ * @param periodMilliseconds Period in milliseconds.
+ * @return Period in tenths of a second.
+ */
+ public static int packAverageWindPeriod(long periodMilliseconds) {
+ return (int) (periodMilliseconds / 100);
+ }
+
+ /**
+ * Unpacks an average wind period, in tenths of a second, into an long, in milliseconds.
+ * @param periodTenthsOfSecond Period in tenths of a second.
+ * @return Period in milliseconds
+ */
+ public static long unpackAverageWindPeriod(int periodTenthsOfSecond) {
+ return (periodTenthsOfSecond * 100);
+ }
+
+
}
diff --git a/racevisionGame/src/main/java/shared/dataInput/EmptyBoatDataSource.java b/racevisionGame/src/main/java/shared/dataInput/EmptyBoatDataSource.java
new file mode 100644
index 00000000..1de4251e
--- /dev/null
+++ b/racevisionGame/src/main/java/shared/dataInput/EmptyBoatDataSource.java
@@ -0,0 +1,47 @@
+package shared.dataInput;
+
+import shared.model.Boat;
+import shared.model.Mark;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An empty {@link BoatDataSource}. Can be used to initialise a race with no data.
+ */
+public class EmptyBoatDataSource implements BoatDataSource {
+
+ /**
+ * A map of source ID to boat for all boats in the race.
+ */
+ private final Map boatMap = new HashMap<>();
+
+ /**
+ * A map of source ID to mark for all marks in the race.
+ */
+ private final Map markerMap = new HashMap<>();
+
+
+
+ public EmptyBoatDataSource() {
+ }
+
+
+ /**
+ * Get the boats that are going to participate in this race
+ * @return Dictionary of boats that are to participate in this race indexed by SourceID
+ */
+ @Override
+ public Map getBoats() {
+ return boatMap;
+ }
+
+ /**
+ * Get the marker Boats that are participating in this race
+ * @return Dictionary of the Markers Boats that are in this race indexed by their Source ID.
+ */
+ @Override
+ public Map getMarkerBoats() {
+ return markerMap;
+ }
+}
diff --git a/racevisionGame/src/main/java/shared/dataInput/EmptyRaceDataSource.java b/racevisionGame/src/main/java/shared/dataInput/EmptyRaceDataSource.java
new file mode 100644
index 00000000..74ccbaf5
--- /dev/null
+++ b/racevisionGame/src/main/java/shared/dataInput/EmptyRaceDataSource.java
@@ -0,0 +1,129 @@
+package shared.dataInput;
+
+import network.Messages.Enums.RaceTypeEnum;
+import shared.model.CompoundMark;
+import shared.model.GPSCoordinate;
+import shared.model.Leg;
+
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An empty {@link RaceDataSource}. Can be used to initialise a race with no data.
+ */
+public class EmptyRaceDataSource implements RaceDataSource {
+
+
+ /**
+ * The GPS coordinate of the top left of the race boundary.
+ */
+ private GPSCoordinate mapTopLeft = new GPSCoordinate(0, 0);
+
+ /**
+ * The GPS coordinate of the bottom right of the race boundary.
+ */
+ private GPSCoordinate mapBottomRight = new GPSCoordinate(0, 0);
+
+
+ /**
+ * A list of GPS coordinates that make up the boundary of the race.
+ */
+ private final List boundary = new ArrayList<>();
+
+ /**
+ * A map between compoundMarkID and a CompoundMark for all CompoundMarks in a race.
+ */
+ private final Map compoundMarkMap = new HashMap<>();
+
+ /**
+ * A list of boat sourceIDs participating in the race.
+ */
+ private final List participants = new ArrayList<>();
+
+ /**
+ * A list of legs in the race.
+ */
+ private final List legs = new ArrayList<>();
+
+
+ /**
+ * The time that the race.xml file was created.
+ */
+ private ZonedDateTime creationTimeDate = ZonedDateTime.now();
+
+ /**
+ * The time that the race should start at, if it hasn't been postponed.
+ */
+ private ZonedDateTime raceStartTime = ZonedDateTime.now().plusMinutes(5);
+
+ /**
+ * Whether or not the race has been postponed.
+ */
+ private boolean postpone = false;
+
+
+ /**
+ * The ID number of the race.
+ */
+ private int raceID = 0;
+
+ /**
+ * The type of the race.
+ */
+ private RaceTypeEnum raceType = RaceTypeEnum.NOT_A_RACE_TYPE;
+
+
+
+ public EmptyRaceDataSource() {
+ }
+
+
+
+ public List getBoundary() {
+ return boundary;
+ }
+
+ public GPSCoordinate getMapTopLeft() {
+ return mapTopLeft;
+ }
+
+ public GPSCoordinate getMapBottomRight() {
+ return mapBottomRight;
+ }
+
+ public List getLegs() {
+ return legs;
+ }
+
+ public List getCompoundMarks() {
+ return new ArrayList<>(compoundMarkMap.values());
+ }
+
+
+ public ZonedDateTime getCreationDateTime() {
+ return creationTimeDate;
+ }
+
+ public ZonedDateTime getStartDateTime() {
+ return raceStartTime;
+ }
+
+ public int getRaceId() {
+ return raceID;
+ }
+
+ public RaceTypeEnum getRaceType() {
+ return raceType;
+ }
+
+ public boolean getPostponed() {
+ return postpone;
+ }
+
+ public List getParticipants() {
+ return participants;
+ }
+}
diff --git a/racevisionGame/src/main/java/shared/dataInput/EmptyRegattaDataSource.java b/racevisionGame/src/main/java/shared/dataInput/EmptyRegattaDataSource.java
new file mode 100644
index 00000000..05a0fcf9
--- /dev/null
+++ b/racevisionGame/src/main/java/shared/dataInput/EmptyRegattaDataSource.java
@@ -0,0 +1,122 @@
+package shared.dataInput;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+import shared.enums.XMLFileType;
+import shared.exceptions.InvalidRegattaDataException;
+import shared.exceptions.XMLReaderException;
+import shared.model.GPSCoordinate;
+
+import java.io.InputStream;
+
+/**
+ * An empty {@link RegattaDataSource}. Can be used to initialise a race with no data.
+ */
+public class EmptyRegattaDataSource implements RegattaDataSource {
+ /**
+ * The regatta ID.
+ */
+ private int regattaID = 0;
+
+ /**
+ * The regatta name.
+ */
+ private String regattaName = "";
+
+ /**
+ * The race ID.
+ */
+ private int raceID = 0;
+
+ /**
+ * The course name.
+ */
+ private String courseName = "";
+
+ /**
+ * The central latitude of the course.
+ */
+ private double centralLatitude = 0;
+
+ /**
+ * The central longitude of the course.
+ */
+ private double centralLongitude = 0;
+
+ /**
+ * The central altitude of the course.
+ */
+ private double centralAltitude = 0;
+
+ /**
+ * The UTC offset of the course.
+ */
+ private float utcOffset = 0;
+
+ /**
+ * The magnetic variation of the course.
+ */
+ private float magneticVariation = 0;
+
+
+
+
+ public EmptyRegattaDataSource() {
+ }
+
+
+
+
+ public int getRegattaID() {
+ return regattaID;
+ }
+
+
+ public String getRegattaName() {
+ return regattaName;
+ }
+
+
+ public int getRaceID() {
+ return raceID;
+ }
+
+
+ public String getCourseName() {
+ return courseName;
+ }
+
+
+ public double getCentralLatitude() {
+ return centralLatitude;
+ }
+
+
+ public double getCentralLongitude() {
+ return centralLongitude;
+ }
+
+
+ public double getCentralAltitude() {
+ return centralAltitude;
+ }
+
+
+ public float getUtcOffset() {
+ return utcOffset;
+ }
+
+
+ public float getMagneticVariation() {
+ return magneticVariation;
+ }
+
+
+ /**
+ * Returns the GPS coorindates of the centre of the regatta.
+ * @return The gps coordinate for the centre of the regatta.
+ */
+ public GPSCoordinate getGPSCoordinate() {
+ return new GPSCoordinate(centralLatitude, centralLongitude);
+ }
+}
diff --git a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java
index 7e61b3de..02e1afc6 100644
--- a/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java
+++ b/racevisionGame/src/main/java/shared/dataInput/RaceXMLReader.java
@@ -5,6 +5,7 @@ import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
+import shared.enums.RoundingType;
import shared.enums.XMLFileType;
import shared.exceptions.InvalidRaceDataException;
import shared.exceptions.XMLReaderException;
@@ -313,6 +314,8 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
return element.getAttribute("Name");
}
+ private String getCompoundMarkRounding(Element element){return element.getAttribute("Rounding");}
+
/**
* Populates list of legs given CompoundMarkSequence element and referenced CompoundMark elements.
@@ -331,12 +334,19 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
//Gets the ID number of this corner element.
int cornerID = getCompoundMarkID(cornerElement);
+ //gets the Rounding of this corner element
+ String cornerRounding = getCompoundMarkRounding(cornerElement);
+
//Gets the CompoundMark associated with this corner.
CompoundMark lastCompoundMark = this.compoundMarkMap.get(cornerID);
//The name of the leg is the name of the first compoundMark in the leg.
String legName = lastCompoundMark.getName();
+ //Sets the rounding type of this compound mark
+
+ lastCompoundMark.setRoundingType(RoundingType.getValueOf(cornerRounding));
+
//For each following corner, create a leg between cornerN and cornerN+1.
for(int i = 1; i < corners.getLength(); i++) {
@@ -346,9 +356,15 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
//Gets the ID number of this corner element.
cornerID = getCompoundMarkID(cornerElement);
+ //gets the Rounding of this corner element
+ cornerRounding = getCompoundMarkRounding(cornerElement);
+
//Gets the CompoundMark associated with this corner.
CompoundMark currentCompoundMark = this.compoundMarkMap.get(cornerID);
+ //Sets the rounding type of this compound mark
+ currentCompoundMark.setRoundingType(RoundingType.getValueOf(cornerRounding));
+
//Create a leg from these two adjacent compound marks.
Leg leg = new Leg(legName, lastCompoundMark, currentCompoundMark, i - 1);
legs.add(leg);
diff --git a/racevisionGame/src/main/java/shared/dataInput/XMLReader.java b/racevisionGame/src/main/java/shared/dataInput/XMLReader.java
index 04c6c1f7..dd5a4d6d 100644
--- a/racevisionGame/src/main/java/shared/dataInput/XMLReader.java
+++ b/racevisionGame/src/main/java/shared/dataInput/XMLReader.java
@@ -166,10 +166,9 @@ public abstract class XMLReader {
* @param path path of the XML
* @param encoding encoding of the xml
* @return A string containing the contents of the specified file.
- * @throws TransformerException Issue with the XML format
* @throws XMLReaderException Thrown if file cannot be read for some reason.
*/
- public static String readXMLFileToString(String path, Charset encoding) throws TransformerException, XMLReaderException {
+ public static String readXMLFileToString(String path, Charset encoding) throws XMLReaderException {
InputStream fileStream = XMLReader.class.getClassLoader().getResourceAsStream(path);
@@ -182,7 +181,11 @@ public abstract class XMLReader {
doc.getDocumentElement().normalize();
- return XMLReader.getContents(doc);
+ try {
+ return XMLReader.getContents(doc);
+ } catch (TransformerException e) {
+ throw new XMLReaderException("Could not get XML file contents.", e);
+ }
}
diff --git a/racevisionGame/src/main/java/shared/enums/RoundingType.java b/racevisionGame/src/main/java/shared/enums/RoundingType.java
new file mode 100644
index 00000000..8f8e719a
--- /dev/null
+++ b/racevisionGame/src/main/java/shared/enums/RoundingType.java
@@ -0,0 +1,49 @@
+package shared.enums;
+
+/**
+ * Enum for the types of rounding that can be done
+ */
+public enum RoundingType {
+ /**
+ * This is means it must be rounded port side
+ */
+ Port,
+
+ /**
+ * This is means it must be rounded starboard side
+ */
+ Starboard,
+
+ /**
+ * The boat within the compound mark with the SeqID
+ * of 1 should be rounded to starboard and the boat
+ * within the compound mark with the SeqID of 2 should
+ * be rounded to port.
+ */
+ SP,
+
+ /**
+ * The boat within the compound mark with the SeqID
+ * of 1 should be rounded to port and the boat
+ * within the compound mark with the SeqID of 2 should
+ * be rounded to starboard.
+ *
+ * opposite of SP
+ */
+ PS;
+
+ public static RoundingType getValueOf(String value) {
+ switch (value) {
+ case "Port":
+ return RoundingType.Port;
+ case "Starboard":
+ return RoundingType.Starboard;
+ case "SP":
+ return RoundingType.Port;
+ case "PS":
+ return RoundingType.Starboard;
+ default:
+ return null;
+ }
+ }
+}
diff --git a/racevisionGame/src/main/java/shared/exceptions/BoatNotFoundException.java b/racevisionGame/src/main/java/shared/exceptions/BoatNotFoundException.java
new file mode 100644
index 00000000..f3fed55c
--- /dev/null
+++ b/racevisionGame/src/main/java/shared/exceptions/BoatNotFoundException.java
@@ -0,0 +1,15 @@
+package shared.exceptions;
+
+/**
+ * An exception thrown when a specific boat cannot be found.
+ */
+public class BoatNotFoundException extends Exception {
+
+ public BoatNotFoundException(String message) {
+ super(message);
+ }
+
+ public BoatNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/racevisionGame/src/main/java/shared/exceptions/HandshakeException.java b/racevisionGame/src/main/java/shared/exceptions/HandshakeException.java
new file mode 100644
index 00000000..2f62e286
--- /dev/null
+++ b/racevisionGame/src/main/java/shared/exceptions/HandshakeException.java
@@ -0,0 +1,24 @@
+package shared.exceptions;
+
+/**
+ * An exception thrown when we the client-server handshake fails.
+ */
+public class HandshakeException extends Exception {
+
+ /**
+ * Constructs the exception with a given message.
+ * @param message Message to store.
+ */
+ public HandshakeException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs the exception with a given message and cause.
+ * @param message Message to store.
+ * @param cause Cause to store.
+ */
+ public HandshakeException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/racevisionGame/src/main/java/shared/exceptions/MarkNotFoundException.java b/racevisionGame/src/main/java/shared/exceptions/MarkNotFoundException.java
new file mode 100644
index 00000000..49cf4f5c
--- /dev/null
+++ b/racevisionGame/src/main/java/shared/exceptions/MarkNotFoundException.java
@@ -0,0 +1,15 @@
+package shared.exceptions;
+
+/**
+ * An exception thrown when a specific mark cannot be found.
+ */
+public class MarkNotFoundException extends Exception {
+
+ public MarkNotFoundException(String message) {
+ super(message);
+ }
+
+ public MarkNotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/racevisionGame/src/main/java/shared/model/Boat.java b/racevisionGame/src/main/java/shared/model/Boat.java
index d6e28783..385c4f3b 100644
--- a/racevisionGame/src/main/java/shared/model/Boat.java
+++ b/racevisionGame/src/main/java/shared/model/Boat.java
@@ -82,6 +82,7 @@ public class Boat {
/**
* The time at which the boat is estimated to reach the next mark, in milliseconds since unix epoch.
*/
+ @Nullable
private ZonedDateTime estimatedTimeAtNextMark;
/**
@@ -106,6 +107,8 @@ public class Boat {
this.bearing = Bearing.fromDegrees(0d);
+ setCurrentPosition(new GPSCoordinate(0, 0));
+
this.status = BoatStatusEnum.UNDEFINED;
}
@@ -365,6 +368,7 @@ public class Boat {
* Returns the time at which the boat should reach the next mark.
* @return Time at which the boat should reach next mark.
*/
+ @Nullable
public ZonedDateTime getEstimatedTimeAtNextMark() {
return estimatedTimeAtNextMark;
}
diff --git a/racevisionGame/src/main/java/shared/model/CompoundMark.java b/racevisionGame/src/main/java/shared/model/CompoundMark.java
index 29bbd10f..66e682e5 100644
--- a/racevisionGame/src/main/java/shared/model/CompoundMark.java
+++ b/racevisionGame/src/main/java/shared/model/CompoundMark.java
@@ -5,6 +5,8 @@ import mock.model.MockBoat;
import mock.model.collider.Collider;
import mock.model.collider.Collision;
+import shared.enums.RoundingType;
+
/**
* Represents a compound mark - that is, either one or two individual marks which form a single compound mark.
*/
@@ -35,6 +37,11 @@ public class CompoundMark implements Collider {
*/
private GPSCoordinate averageGPSCoordinate;
+ /**
+ * The side that the mark must be rounded on
+ */
+ private RoundingType roundingType;
+
/**
* Constructs a compound mark from a single mark.
@@ -170,4 +177,120 @@ public class CompoundMark implements Collider {
if(e.getBearing().degrees() < 90) System.out.println("Starboard");
else if(e.getBearing().degrees() > 270) System.out.println("Port");
}
+
+ /**
+ * Used to find how far apart the marks that make up this gate are
+ * If this compound mark is only one point return base length of 250m
+ * @return the acceptable distance to round a mark
+ */
+ public double getRoundingDistance(){
+ if (mark2 != null){
+ return GPSCoordinate.calculateDistanceMeters(mark1.getPosition(), mark2.getPosition());
+ }else{
+ return 400;
+ }
+ }
+
+ /**
+ * Used to get how this mark should be rounded
+ * @return rounding type for mark
+ */
+ public RoundingType getRoundingType() {
+ return roundingType;
+ }
+
+ /**
+ * Used to set the type of rounding for this mark
+ * @param roundingType rounding type to set
+ */
+ public void setRoundingType(RoundingType roundingType) {
+ this.roundingType = roundingType;
+ }
+
+ /**
+ * Used to find the mark that is to be rounded at a gate when approaching from the south
+ * will also give the single mark if there is only one
+ * @param bearing the bearing a boat will approach form
+ * @return the mark to round
+ */
+ public Mark getMarkForRounding(Bearing bearing){
+ Mark westMostMark;
+ Mark eastMostMark;
+ Mark northMostMark;
+ Mark southMostMark;
+
+ //check to see if there are two marks
+ if (mark2 == null){
+ return mark1;
+ }
+
+ //finds the mark furthest west and east
+ if(this.getMark1Position().getLatitude() > this.getMark2Position().getLatitude()){
+ westMostMark = this.mark1;
+ eastMostMark = this.mark2;
+ }else{
+ westMostMark = this.mark2;
+ eastMostMark = this.mark1;
+ }
+
+ //finds the mark furthest north and south
+ if(this.getMark1Position().getLongitude() > this.getMark2Position().getLongitude()){
+ northMostMark = this.mark1;
+ southMostMark = this.mark2;
+ }else{
+ northMostMark = this.mark2;
+ southMostMark = this.mark1;
+ }
+
+ if (bearing.degrees() > 315 || bearing.degrees() <= 45){
+ //north
+ switch (this.getRoundingType()){
+ case SP:
+ case Port:
+ return westMostMark;
+ case PS:
+ case Starboard:
+ return eastMostMark;
+ default:return null;
+ }
+ }else if(bearing.degrees() > 45 && bearing.degrees() <= 135){
+ //east
+ switch (this.getRoundingType()){
+ case SP:
+ case Port:
+ return northMostMark;
+ case PS:
+ case Starboard:
+ return southMostMark;
+ default:return null;
+ }
+ }else if(bearing.degrees() > 135 && bearing.degrees() <= 225){
+ //south
+ switch (this.getRoundingType()){
+ case SP:
+ case Port:
+ return eastMostMark;
+ case PS:
+ case Starboard:
+ return westMostMark;
+ default:return null;
+ }
+ }else if(bearing.degrees() > 225 && bearing.degrees() <= 315){
+ //west
+ switch (this.getRoundingType()){
+ case SP:
+ case Port:
+ return southMostMark;
+ case PS:
+ case Starboard:
+ return northMostMark;
+ default:return null;
+ }
+ }else{
+ return null;
+ }
+
+ }
+
+
}
diff --git a/racevisionGame/src/main/java/shared/model/FrameRateTracker.java b/racevisionGame/src/main/java/shared/model/FrameRateTracker.java
new file mode 100644
index 00000000..ba683f01
--- /dev/null
+++ b/racevisionGame/src/main/java/shared/model/FrameRateTracker.java
@@ -0,0 +1,109 @@
+package shared.model;
+
+
+import javafx.animation.AnimationTimer;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
+
+
+/**
+ * This class is used to track the framerate of something.
+ * Use {@link #incrementFps(long)} to update it, and {@link #fpsProperty()} to observe it.
+ */
+public class FrameRateTracker {
+
+
+ /**
+ * The number of frames per second.
+ * We essentially track the number of frames generated per second, over a one second period. When {@link #lastFpsResetTime} reaches 1 second, {@link #currentFps} is reset.
+ */
+ private int currentFps = 0;
+
+ /**
+ * The number of frames per second we generated over the last 1 second period.
+ */
+ private IntegerProperty lastFps = new SimpleIntegerProperty(0);
+
+ /**
+ * The time, in milliseconds, since we last reset our {@link #currentFps} counter.
+ */
+ private long lastFpsResetTime;
+
+
+ /**
+ * Creates a {@link FrameRateTracker}. Use {@link #incrementFps(long)} to update it, and {@link #fpsProperty()} to observe it.
+ */
+ public FrameRateTracker() {
+ timer.start();
+ }
+
+
+ /**
+ * Returns the number of frames generated per second.
+ * @return Frames per second.
+ */
+ public int getFps() {
+ return lastFps.getValue();
+ }
+
+ /**
+ * Returns the fps property.
+ * @return The fps property.
+ */
+ public IntegerProperty fpsProperty() {
+ return lastFps;
+ }
+
+
+ /**
+ * Increments the FPS counter, and adds timePeriod milliseconds to our FPS reset timer.
+ * @param timePeriod Time, in milliseconds, to add to {@link #lastFpsResetTime}.
+ */
+ private void incrementFps(long timePeriod) {
+ //Increment.
+ this.currentFps++;
+
+ //Add period to timer.
+ this.lastFpsResetTime += timePeriod;
+
+ //If we have reached 1 second period, snapshot the framerate and reset.
+ if (this.lastFpsResetTime > 1000) {
+ this.lastFps.set(this.currentFps);
+
+ this.currentFps = 0;
+ this.lastFpsResetTime = 0;
+ }
+ }
+
+
+ /**
+ * Timer used to update the framerate.
+ * This is used because we care about frames in the javaFX thread.
+ */
+ private AnimationTimer timer = new AnimationTimer() {
+
+ long previousFrameTime = System.currentTimeMillis();
+
+ @Override
+ public void handle(long now) {
+
+ long currentFrameTime = System.currentTimeMillis();
+ long framePeriod = currentFrameTime - previousFrameTime;
+
+ //Increment fps.
+ incrementFps(framePeriod);
+
+ previousFrameTime = currentFrameTime;
+ }
+ };
+
+
+ /**
+ * Stops the {@link FrameRateTracker}'s timer.
+ */
+ public void stop() {
+ timer.stop();
+ }
+
+
+}
diff --git a/racevisionGame/src/main/java/shared/model/GPSCoordinate.java b/racevisionGame/src/main/java/shared/model/GPSCoordinate.java
index ee5eaaff..8bd1511d 100644
--- a/racevisionGame/src/main/java/shared/model/GPSCoordinate.java
+++ b/racevisionGame/src/main/java/shared/model/GPSCoordinate.java
@@ -142,7 +142,7 @@ public class GPSCoordinate {
* @param coordinate The coordinate to test.
* @return true if a line from the point intersects the two boundary points
*/
- private static boolean intersects(GPSCoordinate boundaryA, GPSCoordinate boundaryB, GPSCoordinate coordinate) {
+ public static boolean intersects(GPSCoordinate boundaryA, GPSCoordinate boundaryB, GPSCoordinate coordinate) {
double boundaryALat = boundaryA.getLatitude();
double boundaryALon = boundaryA.getLongitude();
double boundaryBLat = boundaryB.getLatitude();
@@ -170,6 +170,46 @@ public class GPSCoordinate {
}
+ /**
+ * Checks to see if a point passes or lands on a line
+ * @param linePointA first point for a line
+ * @param linePointB second point for a line
+ * @param point point to check
+ * @param directionBearing direction of the correct side of line
+ * @return true if on the correct side
+ */
+ public static boolean passesLine(GPSCoordinate linePointA, GPSCoordinate linePointB, GPSCoordinate point, Bearing directionBearing) {
+ double d = lineCheck(linePointA, linePointB, point);//this gives a number < 0 for one side and > 0 for an other
+
+ //to find if the side is the correct one
+ //compare with point that is known on the correct side
+ GPSCoordinate pointForComparison = GPSCoordinate.calculateNewPosition(linePointA,
+ 250, Azimuth.fromDegrees(directionBearing.degrees()));
+ double d2 = lineCheck(linePointA, linePointB, pointForComparison);
+
+ return (d > 0 && d2 > 0) || (d < 0 && d2 < 0) || d == 0;
+ }
+
+ /**
+ * returns a double that is positive or negative based on which
+ * side of the line it is on. returns 0 if it is on the line
+ * @param linePointA first point to make up the line
+ * @param linePointB second point to make up the line
+ * @param point the point to check
+ * @return greater than 0 for one side, less than 0 for another
+ */
+ private static double lineCheck(GPSCoordinate linePointA, GPSCoordinate linePointB, GPSCoordinate point) {
+ double linePointALat = linePointA.getLatitude();
+ double linePointALon = linePointA.getLongitude();
+ double linePointBLat = linePointB.getLatitude();
+ double linePointBLon = linePointB.getLongitude();
+ double pointLat = point.getLatitude();
+ double pointLon = point.getLongitude();
+
+ double d1 = (pointLat - linePointALat) * (linePointBLon - linePointALon);
+ double d2 = (pointLon - linePointALon) * (linePointBLat - linePointALat);
+ return d1 - d2; //this gives a number < 0 for one side and > 0 for an other
+ }
/**
diff --git a/racevisionGame/src/main/java/shared/model/Mark.java b/racevisionGame/src/main/java/shared/model/Mark.java
index 5781861a..2b01184a 100644
--- a/racevisionGame/src/main/java/shared/model/Mark.java
+++ b/racevisionGame/src/main/java/shared/model/Mark.java
@@ -22,7 +22,6 @@ public class Mark {
private GPSCoordinate position;
-
/**
* Constructs a mark with a given source ID, name, and position.
* @param sourceID The source ID of the mark.
@@ -35,6 +34,15 @@ public class Mark {
this.position = position;
}
+ /**
+ * Used to create marks that are not visible in the race
+ * @param position position of the mark
+ * @return the new mark
+ */
+ public static Mark tempMark(GPSCoordinate position){
+ return new Mark(-1, "Hidden Mark", position);
+ }
+
/**
* Returns the name of the mark.
diff --git a/racevisionGame/src/main/java/shared/model/RaceState.java b/racevisionGame/src/main/java/shared/model/RaceState.java
new file mode 100644
index 00000000..19a1a52c
--- /dev/null
+++ b/racevisionGame/src/main/java/shared/model/RaceState.java
@@ -0,0 +1,359 @@
+package shared.model;
+
+import javafx.beans.property.Property;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import network.Messages.Enums.RaceStatusEnum;
+import network.Messages.Enums.RaceTypeEnum;
+import shared.dataInput.BoatDataSource;
+import shared.dataInput.RaceDataSource;
+import shared.dataInput.RegattaDataSource;
+
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Represents a yacht race.
+ * This is a base class inherited by {@link mock.model.MockRace} and {@link visualiser.model.VisualiserRaceState}.
+ * Has a course, state, wind, boundaries, etc.... Boats are added by inheriting classes (see {@link Boat}, {@link mock.model.MockBoat}, {@link visualiser.model.VisualiserBoat}.
+ */
+public abstract class RaceState {
+
+
+
+ /**
+ * Data source for race information.
+ */
+ private RaceDataSource raceDataSource;
+
+ /**
+ * Data source for boat information.
+ */
+ private BoatDataSource boatDataSource;
+
+ /**
+ * Data source for regatta information.
+ */
+ private RegattaDataSource regattaDataSource;
+
+ /**
+ * Legs in the race.
+ * We have this in a separate list so that it can be observed.
+ */
+ private ObservableList legs;
+
+
+
+ /**
+ * The clock which tracks the race's start time, current time, and elapsed duration.
+ */
+ private RaceClock raceClock;
+
+
+ /**
+ * The current status of the race.
+ */
+ private RaceStatusEnum raceStatusEnum;
+
+
+ /**
+ * The race's wind.
+ */
+ private Property raceWind = new SimpleObjectProperty<>();
+
+
+
+
+ /**
+ * Constructs an empty race object.
+ * This is initialised into a "default" state, with no data.
+ */
+ public RaceState() {
+
+ //Legs.
+ this.legs = FXCollections.observableArrayList();
+
+ //Race clock.
+ this.raceClock = new RaceClock(ZonedDateTime.now());
+
+ //Race status.
+ this.setRaceStatusEnum(RaceStatusEnum.NOT_ACTIVE);
+
+ //Wind.
+ this.setWind(Bearing.fromDegrees(0), 0);
+
+ }
+
+
+
+ /**
+ * Initialise the boats in the race.
+ * This sets their starting positions and current legs.
+ */
+ protected abstract void initialiseBoats();
+
+
+ /**
+ * Updates the race to use a new list of legs, and adds a dummy "Finish" leg at the end.
+ * @param legs The new list of legs to use.
+ */
+ protected void useLegsList(List legs) {
+ this.legs.setAll(legs);
+ //We add a "dummy" leg at the end of the race.
+ if (getLegs().size() > 0) {
+ getLegs().add(new Leg("Finish", getLegs().size()));
+ }
+ }
+
+
+ /**
+ * Determines whether or not a specific leg is the last leg in the race.
+ * @param leg The leg to check.
+ * @return Returns true if it is the last, false otherwise.
+ */
+ protected boolean isLastLeg(Leg leg) {
+
+ //Get the last leg.
+ Leg lastLeg = getLegs().get(getLegs().size() - 1);
+
+ //Check its ID.
+ int lastLegID = lastLeg.getLegNumber();
+
+ //Get the specified leg's ID.
+ int legID = leg.getLegNumber();
+
+
+ //Check if they are the same.
+ return legID == lastLegID;
+ }
+
+
+ /**
+ * Sets the race data source for the race.
+ * @param raceDataSource New race data source.
+ */
+ public void setRaceDataSource(RaceDataSource raceDataSource) {
+ this.raceDataSource = raceDataSource;
+ this.getRaceClock().setStartingTime(raceDataSource.getStartDateTime());
+ useLegsList(raceDataSource.getLegs());
+ }
+
+ /**
+ * Sets the boat data source for the race.
+ * @param boatDataSource New boat data source.
+ */
+ public void setBoatDataSource(BoatDataSource boatDataSource) {
+ this.boatDataSource = boatDataSource;
+ }
+
+ /**
+ * Sets the regatta data source for the race.
+ * @param regattaDataSource New regatta data source.
+ */
+ public void setRegattaDataSource(RegattaDataSource regattaDataSource) {
+ this.regattaDataSource = regattaDataSource;
+ }
+
+
+ /**
+ * Returns the race data source for the race.
+ * @return Race data source.
+ */
+ public RaceDataSource getRaceDataSource() {
+ return raceDataSource;
+ }
+
+ /**
+ * Returns the race data source for the race.
+ * @return Race data source.
+ */
+ public BoatDataSource getBoatDataSource() {
+ return boatDataSource;
+ }
+
+ /**
+ * Returns the race data source for the race.
+ * @return Race data source.
+ */
+ public RegattaDataSource getRegattaDataSource() {
+ return regattaDataSource;
+ }
+
+
+ /**
+ * Returns a list of {@link Mark} boats.
+ * @return List of mark boats.
+ */
+ public List getMarks() {
+ return new ArrayList<>(boatDataSource.getMarkerBoats().values());
+ }
+
+ /**
+ * Returns a list of sourceIDs participating in the race.
+ * @return List of sourceIDs participating in the race.
+ */
+ public List getParticipants() {
+ return raceDataSource.getParticipants();
+ }
+
+
+
+ /**
+ * Returns the current race status.
+ * @return The current race status.
+ */
+ public RaceStatusEnum getRaceStatusEnum() {
+ return raceStatusEnum;
+ }
+
+ /**
+ * Sets the current race status.
+ * @param raceStatusEnum The new status of the race.
+ */
+ public void setRaceStatusEnum(RaceStatusEnum raceStatusEnum) {
+ this.raceStatusEnum = raceStatusEnum;
+ }
+
+
+ /**
+ * Returns the type of race this is.
+ * @return The type of race this is.
+ */
+ public RaceTypeEnum getRaceType() {
+ return raceDataSource.getRaceType();
+ }
+
+
+ /**
+ * Returns the name of the regatta.
+ * @return The name of the regatta.
+ */
+ public String getRegattaName() {
+ return regattaDataSource.getRegattaName();
+ }
+
+
+ /**
+ * Updates the race to have a specified wind bearing and speed.
+ * @param windBearing New wind bearing.
+ * @param windSpeedKnots New wind speed, in knots.
+ */
+ public void setWind(Bearing windBearing, double windSpeedKnots) {
+ Wind wind = new Wind(windBearing, windSpeedKnots);
+ setWind(wind);
+ }
+
+ /**
+ * Updates the race to have a specified wind (bearing and speed).
+ * @param wind New wind.
+ */
+ public void setWind(Wind wind) {
+ this.raceWind.setValue(wind);
+ }
+
+
+ /**
+ * Returns the wind bearing.
+ * @return The wind bearing.
+ */
+ public Bearing getWindDirection() {
+ return raceWind.getValue().getWindDirection();
+ }
+
+ /**
+ * Returns the wind speed.
+ * Measured in knots.
+ * @return The wind speed.
+ */
+ public double getWindSpeed() {
+ return raceWind.getValue().getWindSpeed();
+ }
+
+ /**
+ * Returns the race's wind.
+ * @return The race's wind.
+ */
+ public Property windProperty() {
+ return raceWind;
+ }
+
+ /**
+ * Returns the RaceClock for this race.
+ * This is used to track the start time, current time, and elapsed duration of the race.
+ * @return The RaceClock for the race.
+ */
+ public RaceClock getRaceClock() {
+ return raceClock;
+ }
+
+
+
+ /**
+ * Returns the number of legs in the race.
+ * @return The number of legs in the race.
+ */
+ public int getLegCount() {
+ //We minus one, as we have added an extra "dummy" leg.
+ return getLegs().size() - 1;
+ }
+
+
+ /**
+ * Returns the race boundary.
+ * @return The race boundary.
+ */
+ public List getBoundary() {
+ return raceDataSource.getBoundary();
+ }
+
+
+ /**
+ * Returns the marks of the race.
+ * @return Marks of the race.
+ */
+ public List getCompoundMarks() {
+ return raceDataSource.getCompoundMarks();
+ }
+
+ /**
+ * Returns the legs of the race.
+ * @return Legs of the race.
+ */
+ public ObservableList getLegs() {
+ return legs;
+ }
+
+
+ /**
+ * Returns the ID of the race.
+ * @return ID of the race.
+ */
+ public int getRaceId() {
+ return raceDataSource.getRaceId();
+ }
+
+
+ /**
+ * Returns the ID of the regatta.
+ * @return The ID of the regatta.
+ */
+ public int getRegattaID() {
+ return regattaDataSource.getRegattaID();
+ }
+
+
+ /**
+ * Returns the name of the course.
+ * @return Name of the course.
+ */
+ public String getCourseName() {
+ return regattaDataSource.getCourseName();
+ }
+
+
+
+
+}
diff --git a/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java b/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java
new file mode 100644
index 00000000..fd5827f0
--- /dev/null
+++ b/racevisionGame/src/main/java/shared/model/RunnableWithFramePeriod.java
@@ -0,0 +1,54 @@
+package shared.model;
+
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This interface is a {@link Runnable} interface, with the ability to sleep until a given time period has elapsed.
+ */
+public interface RunnableWithFramePeriod extends Runnable {
+
+
+
+
+
+
+ /**
+ * Waits for enough time for the period of this frame to be greater than minimumFramePeriod.
+ * @param previousFrameTime The timestamp of the previous frame.
+ * @param currentFrameTime The timestamp of the current frame.
+ * @param minimumFramePeriod The minimum period the frame must be.
+ */
+ default void waitForFramePeriod(long previousFrameTime, long currentFrameTime, long minimumFramePeriod) {
+
+
+ //This is the time elapsed, in milliseconds, since the last server "frame".
+ long framePeriod = currentFrameTime - previousFrameTime;
+
+ //We only attempt to send packets every X milliseconds.
+ if (framePeriod >= minimumFramePeriod) {
+ return;
+
+ } else {
+ //Wait until the frame period will be large enough.
+ long timeToWait = minimumFramePeriod - framePeriod;
+
+ try {
+ Thread.sleep(timeToWait);
+
+ } catch (InterruptedException e) {
+ //If we get interrupted, exit the function.
+ Logger.getGlobal().log(Level.SEVERE, "RunnableWithFramePeriod.waitForFramePeriod().sleep(framePeriod) was interrupted on thread: " + Thread.currentThread(), e);
+ //Re-set the interrupt flag.
+ Thread.currentThread().interrupt();
+ return;
+
+ }
+
+ }
+
+ }
+
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java
new file mode 100644
index 00000000..27c87a08
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ConnectionToServerCommandFactory.java
@@ -0,0 +1,45 @@
+package visualiser.Commands.ConnectionToServerCommands;
+
+
+import mock.exceptions.CommandConstructionException;
+import mock.model.commandFactory.Command;
+import network.Messages.AC35Data;
+import network.Messages.JoinAcceptance;
+import visualiser.network.ConnectionToServer;
+
+/**
+ * Factory to create ConnectionToServer commands.
+ */
+public class ConnectionToServerCommandFactory {
+
+ /**
+ * Generates a command to execute on server connection based on the type of {@link network.Messages.Enums.JoinAcceptanceEnum}.
+ * @param message The message to turn into a command.
+ * @param connectionToServer The connection for the command to operate on.
+ * @return The command to execute the given action.
+ * @throws CommandConstructionException Thrown if the command cannot be constructed.
+ */
+ public static Command create(AC35Data message, ConnectionToServer connectionToServer) throws CommandConstructionException {
+
+ if (!(message instanceof JoinAcceptance)) {
+ throw new CommandConstructionException("Message: " + message + " is not a JoinAcceptance message.");
+ }
+
+ JoinAcceptance joinAcceptance = (JoinAcceptance) message;
+
+
+ switch(joinAcceptance.getAcceptanceType()) {
+
+ case JOIN_SUCCESSFUL_PARTICIPANT: return new JoinSuccessParticipantCommand(joinAcceptance, connectionToServer);
+
+ case JOIN_SUCCESSFUL_SPECTATOR: return new JoinSuccessSpectatorCommand(joinAcceptance, connectionToServer);
+
+ case JOIN_FAILURE: return new JoinFailureCommand(joinAcceptance, connectionToServer);
+
+ case SERVER_FULL: return new ServerFullCommand(joinAcceptance, connectionToServer);
+
+ default: throw new CommandConstructionException("Could not create command for JoinAcceptance: " + joinAcceptance + ". Unknown JoinAcceptanceEnum.");
+ }
+ }
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinFailureCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinFailureCommand.java
new file mode 100644
index 00000000..560509a3
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinFailureCommand.java
@@ -0,0 +1,45 @@
+package visualiser.Commands.ConnectionToServerCommands;
+
+import mock.model.commandFactory.Command;
+import network.Messages.JoinAcceptance;
+import visualiser.enums.ConnectionToServerState;
+import visualiser.network.ConnectionToServer;
+
+
+/**
+ * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_FAILURE} {@link JoinAcceptance} message is received.
+ */
+public class JoinFailureCommand implements Command {
+
+ /**
+ * The message to operate on.
+ */
+ private JoinAcceptance joinAcceptance;
+
+ /**
+ * The context to operate on.
+ */
+ private ConnectionToServer connectionToServer;
+
+
+ /**
+ * Creates a new {@link JoinFailureCommand}, which operates on a given {@link ConnectionToServer}.
+ * @param joinAcceptance The message to operate on.
+ * @param connectionToServer The context to operate on.
+ */
+ public JoinFailureCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
+ this.joinAcceptance = joinAcceptance;
+ this.connectionToServer = connectionToServer;
+ }
+
+
+
+ @Override
+ public void execute() {
+
+ connectionToServer.setJoinAcceptance(joinAcceptance);
+
+ connectionToServer.setConnectionState(ConnectionToServerState.DECLINED);
+
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessParticipantCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessParticipantCommand.java
new file mode 100644
index 00000000..da29664c
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessParticipantCommand.java
@@ -0,0 +1,57 @@
+package visualiser.Commands.ConnectionToServerCommands;
+
+import mock.model.commandFactory.Command;
+import network.Messages.AssignPlayerBoat;
+import network.Messages.JoinAcceptance;
+import visualiser.enums.ConnectionToServerState;
+import visualiser.network.ConnectionToServer;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_SUCCESSFUL_PARTICIPANT} {@link network.Messages.JoinAcceptance} message is received.
+ */
+public class JoinSuccessParticipantCommand implements Command {
+
+ /**
+ * The message to operate on.
+ */
+ private JoinAcceptance joinAcceptance;
+
+ /**
+ * The context to operate on.
+ */
+ private ConnectionToServer connectionToServer;
+
+
+ /**
+ * Creates a new {@link JoinSuccessParticipantCommand}, which operates on a given {@link ConnectionToServer}.
+ * @param joinAcceptance The message to operate on.
+ * @param connectionToServer The context to operate on.
+ */
+ public JoinSuccessParticipantCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
+ this.joinAcceptance = joinAcceptance;
+ this.connectionToServer = connectionToServer;
+ }
+
+
+
+ @Override
+ public void execute() {
+
+ connectionToServer.setJoinAcceptance(joinAcceptance);
+
+ connectionToServer.setConnectionState(ConnectionToServerState.CONNECTED);
+
+
+ AssignPlayerBoat assignPlayerBoat = new AssignPlayerBoat(joinAcceptance.getSourceID());
+ try {
+ connectionToServer.send(assignPlayerBoat);
+ } catch (InterruptedException e) {
+ Logger.getGlobal().log(Level.WARNING, "JoinSuccessParticipantCommand: " + this + " was interrupted on thread: " + Thread.currentThread() + " while sending AssignPlayerBoat message.", e);
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessSpectatorCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessSpectatorCommand.java
new file mode 100644
index 00000000..6e7032fa
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/JoinSuccessSpectatorCommand.java
@@ -0,0 +1,45 @@
+package visualiser.Commands.ConnectionToServerCommands;
+
+import mock.model.commandFactory.Command;
+import network.Messages.JoinAcceptance;
+import visualiser.enums.ConnectionToServerState;
+import visualiser.network.ConnectionToServer;
+
+
+/**
+ * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_SUCCESSFUL_PARTICIPANT} {@link JoinAcceptance} message is received.
+ */
+public class JoinSuccessSpectatorCommand implements Command {
+
+ /**
+ * The message to operate on.
+ */
+ private JoinAcceptance joinAcceptance;
+
+ /**
+ * The context to operate on.
+ */
+ private ConnectionToServer connectionToServer;
+
+
+ /**
+ * Creates a new {@link JoinSuccessSpectatorCommand}, which operates on a given {@link ConnectionToServer}.
+ * @param joinAcceptance The message to operate on.
+ * @param connectionToServer The context to operate on.
+ */
+ public JoinSuccessSpectatorCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
+ this.joinAcceptance = joinAcceptance;
+ this.connectionToServer = connectionToServer;
+ }
+
+
+
+ @Override
+ public void execute() {
+
+ connectionToServer.setJoinAcceptance(joinAcceptance);
+
+ connectionToServer.setConnectionState(ConnectionToServerState.CONNECTED);
+
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ServerFullCommand.java b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ServerFullCommand.java
new file mode 100644
index 00000000..e88d2c16
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/ConnectionToServerCommands/ServerFullCommand.java
@@ -0,0 +1,47 @@
+package visualiser.Commands.ConnectionToServerCommands;
+
+import mock.model.commandFactory.Command;
+import network.Messages.JoinAcceptance;
+import visualiser.enums.ConnectionToServerState;
+import visualiser.network.ConnectionToServer;
+
+import java.util.Optional;
+
+
+/**
+ * Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#SERVER_FULL} {@link JoinAcceptance} message is received.
+ */
+public class ServerFullCommand implements Command {
+
+ /**
+ * The message to operate on.
+ */
+ private JoinAcceptance joinAcceptance;
+
+ /**
+ * The context to operate on.
+ */
+ private ConnectionToServer connectionToServer;
+
+
+ /**
+ * Creates a new {@link ServerFullCommand}, which operates on a given {@link ConnectionToServer}.
+ * @param joinAcceptance The message to operate on.
+ * @param connectionToServer The context to operate on.
+ */
+ public ServerFullCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
+ this.joinAcceptance = joinAcceptance;
+ this.connectionToServer = connectionToServer;
+ }
+
+
+
+ @Override
+ public void execute() {
+
+ connectionToServer.setJoinAcceptance(joinAcceptance);
+
+ connectionToServer.setConnectionState(ConnectionToServerState.DECLINED);
+
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommand.java b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommand.java
new file mode 100644
index 00000000..d4668ef7
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommand.java
@@ -0,0 +1,47 @@
+package visualiser.Commands.IncomingHeartBeatCommands;
+
+import mock.model.commandFactory.Command;
+import network.Messages.HeartBeat;
+import network.Messages.JoinAcceptance;
+import visualiser.enums.ConnectionToServerState;
+import visualiser.network.ConnectionToServer;
+import visualiser.network.IncomingHeartBeatService;
+
+
+/**
+ * Command created when a {@link HeartBeat} message is received.
+ */
+public class IncomingHeartBeatCommand implements Command {
+
+ /**
+ * The message to operate on.
+ */
+ private HeartBeat heartBeat;
+
+ /**
+ * The context to operate on.
+ */
+ private IncomingHeartBeatService incomingHeartBeatService;
+
+
+ /**
+ * Creates a new {@link IncomingHeartBeatCommand}, which operates on a given {@link IncomingHeartBeatService}.
+ * @param heartBeat The message to operate on.
+ * @param incomingHeartBeatService The context to operate on.
+ */
+ public IncomingHeartBeatCommand(HeartBeat heartBeat, IncomingHeartBeatService incomingHeartBeatService) {
+ this.heartBeat = heartBeat;
+ this.incomingHeartBeatService = incomingHeartBeatService;
+ }
+
+
+
+ @Override
+ public void execute() {
+
+ incomingHeartBeatService.setLastHeartBeatSeqNum(heartBeat.getSequenceNumber());
+
+ incomingHeartBeatService.setLastHeartbeatTime(System.currentTimeMillis());
+
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommandFactory.java
new file mode 100644
index 00000000..bab68c42
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/IncomingHeartBeatCommands/IncomingHeartBeatCommandFactory.java
@@ -0,0 +1,33 @@
+package visualiser.Commands.IncomingHeartBeatCommands;
+
+
+import mock.exceptions.CommandConstructionException;
+import mock.model.commandFactory.Command;
+import network.Messages.AC35Data;
+import network.Messages.HeartBeat;
+import visualiser.network.IncomingHeartBeatService;
+
+/**
+ * Factory to create IncomingHeartBeatService commands.
+ */
+public class IncomingHeartBeatCommandFactory {
+
+ /**
+ * Generates a command on an IncomingHeartBeatService.
+ * @param message The message to turn into a command.
+ * @param incomingHeartBeatService The context for the command to operate on.
+ * @return The command to execute the given action.
+ * @throws CommandConstructionException Thrown if the command cannot be constructed.
+ */
+ public static Command create(AC35Data message, IncomingHeartBeatService incomingHeartBeatService) throws CommandConstructionException {
+
+ if (!(message instanceof HeartBeat)) {
+ throw new CommandConstructionException("Message: " + message + " is not a HeartBeat message.");
+ }
+
+ HeartBeat heartBeat = (HeartBeat) message;
+
+ return new IncomingHeartBeatCommand(heartBeat, incomingHeartBeatService);
+ }
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/AssignPlayerBoatCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/AssignPlayerBoatCommand.java
new file mode 100644
index 00000000..fd265325
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/AssignPlayerBoatCommand.java
@@ -0,0 +1,53 @@
+package visualiser.Commands.VisualiserRaceCommands;
+
+import mock.model.commandFactory.Command;
+import network.Messages.AssignPlayerBoat;
+import network.Messages.BoatLocation;
+import shared.exceptions.BoatNotFoundException;
+import shared.exceptions.MarkNotFoundException;
+import shared.model.GPSCoordinate;
+import shared.model.Mark;
+import visualiser.model.VisualiserBoat;
+import visualiser.model.VisualiserRaceState;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Command created when a {@link AssignPlayerBoat} message is received.
+ */
+public class AssignPlayerBoatCommand implements Command {
+
+ /**
+ * The message to operate on.
+ */
+ private AssignPlayerBoat assignPlayerBoat;
+
+ /**
+ * The context to operate on.
+ */
+ private VisualiserRaceState visualiserRace;
+
+
+ /**
+ * Creates a new {@link AssignPlayerBoatCommand}, which operates on a given {@link VisualiserRaceState}.
+ * @param assignPlayerBoat The message to operate on.
+ * @param visualiserRace The context to operate on.
+ */
+ public AssignPlayerBoatCommand(AssignPlayerBoat assignPlayerBoat, VisualiserRaceState visualiserRace) {
+ this.assignPlayerBoat = assignPlayerBoat;
+ this.visualiserRace = visualiserRace;
+ }
+
+
+
+ @Override
+ public void execute() {
+
+ visualiserRace.setPlayerBoatID(assignPlayerBoat.getSourceID());
+
+ }
+
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatLocationCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatLocationCommand.java
new file mode 100644
index 00000000..d2c60bd7
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatLocationCommand.java
@@ -0,0 +1,127 @@
+package visualiser.Commands.VisualiserRaceCommands;
+
+import mock.model.commandFactory.Command;
+import network.Messages.BoatLocation;
+import network.Messages.Enums.BoatStatusEnum;
+import shared.exceptions.BoatNotFoundException;
+import shared.exceptions.MarkNotFoundException;
+import shared.model.GPSCoordinate;
+import shared.model.Mark;
+import visualiser.model.VisualiserBoat;
+import visualiser.model.VisualiserRaceEvent;
+import visualiser.model.VisualiserRaceState;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Command created when a {@link BoatLocation} message is received.
+ */
+public class BoatLocationCommand implements Command {
+
+ /**
+ * The message to operate on.
+ */
+ private BoatLocation boatLocation;
+
+ /**
+ * The context to operate on.
+ */
+ private VisualiserRaceState visualiserRace;
+
+
+ /**
+ * Creates a new {@link BoatLocationCommand}, which operates on a given {@link VisualiserRaceState}.
+ * @param boatLocation The message to operate on.
+ * @param visualiserRace The context to operate on.
+ */
+ public BoatLocationCommand(BoatLocation boatLocation, VisualiserRaceState visualiserRace) {
+ this.boatLocation = boatLocation;
+ this.visualiserRace = visualiserRace;
+ }
+
+
+
+ @Override
+ public void execute() {
+
+ if (visualiserRace.isVisualiserBoat(boatLocation.getSourceID())) {
+ updateBoatLocation();
+ } else if (visualiserRace.isMark(boatLocation.getSourceID())) {
+ updateMarkLocation();
+ }
+
+ }
+
+
+ /**
+ * Updates the boat specified in the message.
+ */
+ private void updateBoatLocation() {
+
+ try {
+ VisualiserBoat boat = visualiserRace.getBoat(boatLocation.getSourceID());
+
+ //Get the new position.
+ GPSCoordinate gpsCoordinate = new GPSCoordinate(
+ boatLocation.getLatitude(),
+ boatLocation.getLongitude());
+
+ boat.setCurrentPosition(gpsCoordinate);
+
+ //Bearing.
+ boat.setBearing(boatLocation.getHeading());
+
+
+ //Speed.
+ boat.setCurrentSpeed(boatLocation.getBoatSpeedKnots());
+
+
+ //Attempt to add a track point.
+ attemptAddTrackPoint(boat);
+
+
+ } catch (BoatNotFoundException e) {
+ Logger.getGlobal().log(Level.WARNING, "BoatLocationCommand: " + this + " could not execute. Boat with sourceID: " + boatLocation.getSourceID() + " not found.", e);
+ return;
+ }
+
+ }
+
+
+ /**
+ * Attempts to add a track point to the boat. Only works if the boat is currently racing.
+ * @param boat The boat to add a track point to.
+ */
+ private void attemptAddTrackPoint(VisualiserBoat boat) {
+ if (boat.getStatus() == BoatStatusEnum.RACING) {
+ boat.addTrackPoint(boat.getCurrentPosition(), visualiserRace.getRaceClock().getCurrentTime());
+ }
+ }
+
+
+ /**
+ * Updates the marker boat specified in message.
+ */
+ private void updateMarkLocation() {
+
+ try {
+ Mark mark = visualiserRace.getMark(boatLocation.getSourceID());
+
+ GPSCoordinate gpsCoordinate = new GPSCoordinate(
+ boatLocation.getLatitude(),
+ boatLocation.getLongitude());
+
+ mark.setPosition(gpsCoordinate);
+ } catch (MarkNotFoundException e) {
+ Logger.getGlobal().log(Level.WARNING, "BoatLocationCommand: " + this + " could not execute. Mark with sourceID: " + boatLocation.getSourceID() + " not found.", e);
+ return;
+
+ }
+
+
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatsXMLMessageCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatsXMLMessageCommand.java
new file mode 100644
index 00000000..7f56d745
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatsXMLMessageCommand.java
@@ -0,0 +1,48 @@
+package visualiser.Commands.VisualiserRaceCommands;
+
+import mock.model.commandFactory.Command;
+import network.Messages.XMLMessage;
+import shared.dataInput.BoatDataSource;
+import shared.dataInput.BoatXMLReader;
+import shared.enums.XMLFileType;
+import shared.exceptions.InvalidBoatDataException;
+import shared.exceptions.XMLReaderException;
+import visualiser.model.VisualiserRaceEvent;
+import visualiser.model.VisualiserRaceState;
+
+
+/**
+ * Command created when a {@link network.Messages.Enums.XMLMessageType#BOAT} {@link XMLMessage} message is received.
+ */
+public class BoatsXMLMessageCommand implements Command {
+
+ /**
+ * The data source to operate on.
+ */
+ private BoatDataSource boatDataSource;
+
+ /**
+ * The context to operate on.
+ */
+ private VisualiserRaceState visualiserRace;
+
+
+ /**
+ * Creates a new {@link BoatsXMLMessageCommand}, which operates on a given {@link VisualiserRaceEvent}.
+ * @param boatDataSource The data source to operate on.
+ * @param visualiserRace The context to operate on.
+ */
+ public BoatsXMLMessageCommand(BoatDataSource boatDataSource, VisualiserRaceState visualiserRace) {
+ this.boatDataSource = boatDataSource;
+ this.visualiserRace = visualiserRace;
+ }
+
+
+
+ @Override
+ public void execute() {
+
+ visualiserRace.setBoatDataSource(boatDataSource);
+
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java
new file mode 100644
index 00000000..825cd274
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java
@@ -0,0 +1,185 @@
+package visualiser.Commands.VisualiserRaceCommands;
+
+import mock.model.commandFactory.Command;
+import network.Messages.BoatStatus;
+import network.Messages.Enums.BoatStatusEnum;
+import network.Messages.RaceStatus;
+import shared.exceptions.BoatNotFoundException;
+import shared.model.Leg;
+import visualiser.model.VisualiserBoat;
+import visualiser.model.VisualiserRaceEvent;
+import visualiser.model.VisualiserRaceState;
+
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Command created when a {@link RaceStatus} message is received.
+ */
+public class RaceStatusCommand implements Command {
+
+ /**
+ * The message to operate on.
+ */
+ private RaceStatus raceStatus;
+
+ /**
+ * The context to operate on.
+ */
+ private VisualiserRaceState visualiserRace;
+
+
+ /**
+ * Creates a new {@link RaceStatusCommand}, which operates on a given {@link VisualiserRaceState}.
+ * @param raceStatus The message to operate on.
+ * @param visualiserRace The context to operate on.
+ */
+ public RaceStatusCommand(RaceStatus raceStatus, VisualiserRaceState visualiserRace) {
+ this.raceStatus = raceStatus;
+ this.visualiserRace = visualiserRace;
+ }
+
+
+
+ @Override
+ public void execute() {
+
+ //Race status enum.
+ visualiserRace.setRaceStatusEnum(raceStatus.getRaceStatus());
+
+ //Wind.
+ visualiserRace.setWind(
+ raceStatus.getWindDirection(),
+ raceStatus.getWindSpeed() );
+
+ //Current race time.
+ visualiserRace.getRaceClock().setUTCTime(raceStatus.getCurrentTime());
+
+
+
+ for (BoatStatus boatStatus : raceStatus.getBoatStatuses()) {
+ updateBoatStatus(boatStatus);
+ }
+
+
+ visualiserRace.updateBoatPositions(visualiserRace.getBoats());
+
+
+
+ }
+
+
+ /**
+ * Updates a single boat's status using the boatStatus message.
+ * @param boatStatus BoatStatus message to get data from.
+ */
+ private void updateBoatStatus(BoatStatus boatStatus) {
+ try {
+ VisualiserBoat boat = visualiserRace.getBoat(boatStatus.getSourceID());
+
+ //Time at next mark.
+ updateEstimatedTimeAtNextMark(boatStatus, boat);
+
+
+ BoatStatusEnum newBoatStatusEnum = boatStatus.getBoatStatus();
+
+ //Time at last mark.
+ initialiseTimeAtLastMark(boat, boat.getStatus(), newBoatStatusEnum);
+
+ //Status.
+ boat.setStatus(newBoatStatusEnum);
+
+
+ List legs = visualiserRace.getLegs();
+
+ //Leg.
+ updateLeg(boatStatus.getLegNumber(), boat, legs);
+
+
+ //Set finish time if boat finished.
+ attemptUpdateFinishTime(boatStatus, boat, legs);
+
+
+ } catch (BoatNotFoundException e) {
+ //Logger.getGlobal().log(Level.WARNING, "RaceStatusCommand.updateBoatStatus: " + this + " could not execute. Boat with sourceID: " + boatStatus.getSourceID() + " not found.", e);
+ return;
+ }
+ }
+
+
+ /**
+ * Attempts to update the finish time of the boat. Only works if the boat has actually finished the race.
+ * @param boatStatus BoatStatus to read data from.
+ * @param boat Boat to update.
+ * @param legs Legs of the race.
+ */
+ private void attemptUpdateFinishTime(BoatStatus boatStatus, VisualiserBoat boat, List legs) {
+
+ if (boat.getStatus() == BoatStatusEnum.FINISHED || boatStatus.getLegNumber() == legs.size()) {
+ boat.setTimeFinished(visualiserRace.getRaceClock().getCurrentTimeMilli());
+ boat.setStatus(BoatStatusEnum.FINISHED);
+ }
+
+ }
+
+
+
+ /**
+ * Updates a boat's leg.
+ * @param legNumber The new leg number.
+ * @param boat The boat to update.
+ * @param legs The legs in the race.
+ */
+ private void updateLeg(int legNumber, VisualiserBoat boat, List legs) {
+
+ if (legNumber >= 1 && legNumber < legs.size()) {
+ if (boat.getCurrentLeg() != legs.get(legNumber)) {
+ boatFinishedLeg(boat, legs.get(legNumber));
+ }
+ }
+ }
+
+ /**
+ * Initialises the time at last mark for a boat. Only changes if the boat's status is changing from non-racing to racing.
+ * @param boat The boat to update.
+ * @param currentBoatStatus The current status of the boat.
+ * @param newBoatStatusEnum The new status of the boat, from the BoatStatus message.
+ */
+ private void initialiseTimeAtLastMark(VisualiserBoat boat, BoatStatusEnum currentBoatStatus, BoatStatusEnum newBoatStatusEnum) {
+ //If we are changing from non-racing to racing, we need to initialise boat with their time at last mark.
+ if ((currentBoatStatus != BoatStatusEnum.RACING) && (newBoatStatusEnum == BoatStatusEnum.RACING)) {
+ boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime());
+ }
+ }
+
+
+ /**
+ * Updates the estimated time at next mark for a given boat.
+ * @param boatStatus BoatStatus to read data from.
+ * @param boat Boat to update.
+ */
+ private void updateEstimatedTimeAtNextMark(BoatStatus boatStatus, VisualiserBoat boat) {
+ boat.setEstimatedTimeAtNextMark(visualiserRace.getRaceClock().getLocalTime(boatStatus.getEstTimeAtNextMark()));
+ }
+
+
+
+ /**
+ * Updates a boat's leg to a specified leg. Also records the order in which the boat passed the leg.
+ * @param boat The boat to update.
+ * @param leg The leg to use.
+ */
+ private void boatFinishedLeg(VisualiserBoat boat, Leg leg) {
+
+ //Record order in which boat finished leg.
+ visualiserRace.getLegCompletionOrder().get(boat.getCurrentLeg()).add(boat);
+
+ //Update boat.
+ boat.setCurrentLeg(leg);
+ boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime());
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceXMLMessageCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceXMLMessageCommand.java
new file mode 100644
index 00000000..0b194674
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceXMLMessageCommand.java
@@ -0,0 +1,44 @@
+package visualiser.Commands.VisualiserRaceCommands;
+
+import mock.model.commandFactory.Command;
+import network.Messages.XMLMessage;
+import shared.dataInput.RaceDataSource;
+import visualiser.model.VisualiserRaceEvent;
+import visualiser.model.VisualiserRaceState;
+
+
+/**
+ * Command created when a {@link network.Messages.Enums.XMLMessageType#BOAT} {@link XMLMessage} message is received.
+ */
+public class RaceXMLMessageCommand implements Command {
+
+ /**
+ * The data source to operate on.
+ */
+ private RaceDataSource raceDataSource;
+
+ /**
+ * The context to operate on.
+ */
+ private VisualiserRaceState visualiserRace;
+
+
+ /**
+ * Creates a new {@link RaceXMLMessageCommand}, which operates on a given {@link VisualiserRaceEvent}.
+ * @param raceDataSource The data source to operate on.
+ * @param visualiserRace The context to operate on.
+ */
+ public RaceXMLMessageCommand(RaceDataSource raceDataSource, VisualiserRaceState visualiserRace) {
+ this.raceDataSource = raceDataSource;
+ this.visualiserRace = visualiserRace;
+ }
+
+
+
+ @Override
+ public void execute() {
+
+ visualiserRace.setRaceDataSource(raceDataSource);
+
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RegattaXMLMessageCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RegattaXMLMessageCommand.java
new file mode 100644
index 00000000..6597e557
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RegattaXMLMessageCommand.java
@@ -0,0 +1,44 @@
+package visualiser.Commands.VisualiserRaceCommands;
+
+import mock.model.commandFactory.Command;
+import network.Messages.XMLMessage;
+import shared.dataInput.RegattaDataSource;
+import visualiser.model.VisualiserRaceEvent;
+import visualiser.model.VisualiserRaceState;
+
+
+/**
+ * Command created when a {@link network.Messages.Enums.XMLMessageType#BOAT} {@link XMLMessage} message is received.
+ */
+public class RegattaXMLMessageCommand implements Command {
+
+ /**
+ * The data source to operate on.
+ */
+ private RegattaDataSource regattaDataSource;
+
+ /**
+ * The context to operate on.
+ */
+ private VisualiserRaceState visualiserRace;
+
+
+ /**
+ * Creates a new {@link RegattaXMLMessageCommand}, which operates on a given {@link VisualiserRaceEvent}.
+ * @param regattaDataSource The data source to operate on.
+ * @param visualiserRace The context to operate on.
+ */
+ public RegattaXMLMessageCommand(RegattaDataSource regattaDataSource, VisualiserRaceState visualiserRace) {
+ this.regattaDataSource = regattaDataSource;
+ this.visualiserRace = visualiserRace;
+ }
+
+
+
+ @Override
+ public void execute() {
+
+ visualiserRace.setRegattaDataSource(regattaDataSource);
+
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java
new file mode 100644
index 00000000..37755e91
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java
@@ -0,0 +1,40 @@
+package visualiser.Commands.VisualiserRaceCommands;
+
+
+import mock.exceptions.CommandConstructionException;
+import mock.model.commandFactory.Command;
+import network.Messages.*;
+import visualiser.model.VisualiserRaceEvent;
+import visualiser.model.VisualiserRaceState;
+
+/**
+ * Factory to create VisualiserRace commands.
+ */
+public class VisualiserRaceCommandFactory {
+
+ /**
+ * Generates a command on an VisualiserRace.
+ * @param message The message to turn into a command.
+ * @param visualiserRace The context for the command to operate on.
+ * @return The command to execute the given action.
+ * @throws CommandConstructionException Thrown if the command cannot be constructed.
+ */
+ public static Command create(AC35Data message, VisualiserRaceState visualiserRace) throws CommandConstructionException {
+
+ switch (message.getType()) {
+
+ case BOATLOCATION: return new BoatLocationCommand((BoatLocation) message, visualiserRace);
+
+ case RACESTATUS: return new RaceStatusCommand((RaceStatus) message, visualiserRace);
+
+ case XMLMESSAGE: return XMLMessageCommandFactory.create((XMLMessage) message, visualiserRace);
+
+ case ASSIGN_PLAYER_BOAT: return new AssignPlayerBoatCommand((AssignPlayerBoat) message, visualiserRace);
+
+ default: throw new CommandConstructionException("Could not create VisualiserRaceCommand. Unrecognised or unsupported MessageType: " + message.getType());
+
+ }
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/XMLMessageCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/XMLMessageCommandFactory.java
new file mode 100644
index 00000000..b9b58f1c
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/XMLMessageCommandFactory.java
@@ -0,0 +1,63 @@
+package visualiser.Commands.VisualiserRaceCommands;
+
+
+import mock.exceptions.CommandConstructionException;
+import mock.model.commandFactory.Command;
+import network.Messages.AC35Data;
+import network.Messages.BoatLocation;
+import network.Messages.RaceStatus;
+import network.Messages.XMLMessage;
+import shared.dataInput.*;
+import shared.enums.XMLFileType;
+import shared.exceptions.InvalidBoatDataException;
+import shared.exceptions.InvalidRaceDataException;
+import shared.exceptions.InvalidRegattaDataException;
+import shared.exceptions.XMLReaderException;
+import visualiser.model.VisualiserRaceState;
+
+/**
+ * Factory to create VisualiserRace commands, from XMLMessages.
+ */
+public class XMLMessageCommandFactory {
+
+ /**
+ * Generates a command on an VisualiserRace.
+ * @param message The message to turn into a command.
+ * @param visualiserRace The context for the command to operate on.
+ * @return The command to execute the given action.
+ * @throws CommandConstructionException Thrown if the command cannot be constructed.
+ */
+ public static Command create(XMLMessage message, VisualiserRaceState visualiserRace) throws CommandConstructionException {
+
+ try {
+
+ switch (message.getXmlMsgSubType()) {
+
+ case BOAT:
+ BoatDataSource boatDataSource = new BoatXMLReader(message.getXmlMessage(), XMLFileType.Contents);
+ return new BoatsXMLMessageCommand(boatDataSource, visualiserRace);
+
+
+ case RACE:
+ RaceDataSource raceDataSource = new RaceXMLReader(message.getXmlMessage(), XMLFileType.Contents);
+ return new RaceXMLMessageCommand(raceDataSource, visualiserRace);
+
+
+ case REGATTA:
+ RegattaDataSource regattaDataSource = new RegattaXMLReader(message.getXmlMessage(), XMLFileType.Contents);
+ return new RegattaXMLMessageCommand(regattaDataSource, visualiserRace);
+
+
+ default:
+ throw new CommandConstructionException("Could not create VisualiserRaceCommand/XMLCommand. Unrecognised or unsupported MessageType: " + message.getType());
+
+ }
+
+ } catch (XMLReaderException | InvalidBoatDataException | InvalidRegattaDataException | InvalidRaceDataException e) {
+ throw new CommandConstructionException("Could not create VisualiserRaceCommand/XMLCommand. Could not parse XML message payload.", e);
+
+ }
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/Controllers/ArrowController.java b/racevisionGame/src/main/java/visualiser/Controllers/ArrowController.java
new file mode 100644
index 00000000..ab9bd421
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/Controllers/ArrowController.java
@@ -0,0 +1,150 @@
+package visualiser.Controllers;
+
+
+import javafx.application.Platform;
+import javafx.beans.property.Property;
+import javafx.fxml.FXML;
+import javafx.scene.control.Label;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.StackPane;
+import javafx.scene.shape.Circle;
+import shared.model.Bearing;
+import shared.model.Wind;
+
+/**
+ * Controller for the arrow.fxml view.
+ */
+public class ArrowController {
+
+
+ @FXML
+ private Pane compass;
+
+ @FXML
+ private StackPane arrowStackPane;
+
+ @FXML
+ private ImageView arrowImage;
+
+ @FXML
+ private Circle circle;
+
+ @FXML
+ private Label northLabel;
+
+ @FXML
+ private Label windLabel;
+
+ @FXML
+ private Label speedLabel;
+
+
+ /**
+ * This is the property our arrow control binds to.
+ */
+ private Property wind;
+
+
+ /**
+ * Constructor.
+ */
+ public ArrowController() {
+ }
+
+
+ /**
+ * Sets which wind property the arrow control should bind to.
+ * @param wind The wind property to bind to.
+ */
+ public void setWindProperty(Property wind) {
+ this.wind = wind;
+
+ wind.addListener((observable, oldValue, newValue) -> {
+ if (newValue != null) {
+ Platform.runLater(() -> updateWind(newValue));
+ }
+ });
+ }
+
+
+ /**
+ * Updates the control to use the new wind value.
+ * This updates the arrow direction (due to bearing), arrow length (due to speed), and label (due to speed).
+ * @param wind The wind value to use.
+ */
+ private void updateWind(Wind wind) {
+ updateWindBearing(wind.getWindDirection());
+ updateWindSpeed(wind.getWindSpeed());
+ }
+
+
+ /**
+ * Updates the control to account for the new wind speed.
+ * This changes the length (height) of the wind arrow, and updates the speed label.
+ * @param speedKnots The new wind speed, in knots.
+ */
+ private void updateWindSpeed(double speedKnots) {
+ updateWindArrowLength(speedKnots);
+ updateWindSpeedLabel(speedKnots);
+ }
+
+ /**
+ * Updates the length of the wind arrow according to the specified wind speed.
+ * @param speedKnots Wind speed, in knots.
+ */
+ private void updateWindArrowLength(double speedKnots) {
+
+ //At 2 knots, the arrow reaches its minimum height, and at 30 knots it reaches its maximum height.
+ double minKnots = 2;
+ double maxKnots = 30;
+ double deltaKnots = maxKnots - minKnots;
+
+ double minHeight = 25;
+ double maxHeight = 75;
+ double deltaHeight = maxHeight - minHeight;
+
+ //Clamp speed.
+ if (speedKnots > maxKnots) {
+ speedKnots = maxKnots;
+ } else if (speedKnots < minKnots) {
+ speedKnots = minKnots;
+ }
+
+ //How far between the knots bounds is the current speed?
+ double currentDeltaKnots = speedKnots - minKnots;
+ double currentKnotsScalar = currentDeltaKnots / deltaKnots;
+
+ //Thus, how far between the pixel height bounds should the arrow height be?
+ double newHeight = minHeight + (currentKnotsScalar * deltaHeight);
+
+ arrowImage.setFitHeight(newHeight);
+ }
+
+ /**
+ * Updates the wind speed label according to the specified wind speed.
+ * @param speedKnots Wind speed, in knots.
+ */
+ private void updateWindSpeedLabel(double speedKnots) {
+ speedLabel.setText(String.format("%.1fkn", speedKnots));
+ }
+
+
+ /**
+ * Updates the control to account for a new wind bearing.
+ * This rotates the arrow according to the bearing.
+ * @param bearing The bearing to use to rotate arrow.
+ */
+ private void updateWindBearing(Bearing bearing) {
+
+ //We need to display wind-from, so add 180 degrees.
+ Bearing fromBearing = Bearing.fromDegrees(bearing.degrees() + 180d);
+
+ //Rotate the wind arrow.
+ arrowStackPane.setRotate(fromBearing.degrees());
+ }
+
+
+
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java
index ae8c682c..a8caad09 100644
--- a/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java
+++ b/racevisionGame/src/main/java/visualiser/Controllers/ConnectionController.java
@@ -19,6 +19,7 @@ import java.net.URL;
import java.net.UnknownHostException;
import java.util.ResourceBundle;
+//TODO it appears this view/controller was replaced by Lobby.fxml. Remove?
/**
* Controls the connection that the VIsualiser can connect to.
*/
@@ -82,11 +83,6 @@ public class ConnectionController extends Controller {
private ObservableList connections;
- /**
- * Represents whether the client is currently hosting a game already - this is to ensure they don't launch multiple servers.
- */
- private boolean currentlyHostingGame = false;
-
@Override
public void initialize(URL location, ResourceBundle resources) {
@@ -144,32 +140,5 @@ public class ConnectionController extends Controller {
}
}
- /**
- * Sets up a new host
- */
- public void addLocal() {
- try {
- //We don't want to host more than one game.
- if (!currentlyHostingGame) {
- Event game = Event.getEvent();
- urlField.textProperty().set(game.getAddress());
- portField.textProperty().set(Integer.toString(game.getPort()));
-
- game.start();
- addConnection();
-
- currentlyHostingGame = true;
- }
- } catch (InvalidRaceDataException e) {
- e.printStackTrace();
- } catch (XMLReaderException e) {
- e.printStackTrace();
- } catch (InvalidBoatDataException e) {
- e.printStackTrace();
- } catch (InvalidRegattaDataException e) {
- e.printStackTrace();
- } catch (UnknownHostException e) {
- e.printStackTrace();
- }
- }
+
}
diff --git a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java
index e87ea689..5966c643 100644
--- a/racevisionGame/src/main/java/visualiser/Controllers/HostController.java
+++ b/racevisionGame/src/main/java/visualiser/Controllers/HostController.java
@@ -6,6 +6,7 @@ import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import mock.app.Event;
+import mock.exceptions.EventConstructionException;
import shared.exceptions.InvalidBoatDataException;
import shared.exceptions.InvalidRaceDataException;
import shared.exceptions.InvalidRegattaDataException;
@@ -17,6 +18,8 @@ import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ResourceBundle;
+import java.util.logging.Level;
+import java.util.logging.Logger;
/**
* Controller for Hosting a game.
@@ -44,17 +47,12 @@ public class HostController extends Controller {
*/
public void hostGamePressed() throws IOException{
try {
- Event game = Event.getEvent();
+ Event game = new Event(false);
game.start();
connectSocket("localhost", 4942);
- } catch (InvalidRaceDataException e) {
- e.printStackTrace();
- } catch (XMLReaderException e) {
- e.printStackTrace();
- } catch (InvalidBoatDataException e) {
- e.printStackTrace();
- } catch (InvalidRegattaDataException e) {
- e.printStackTrace();
+ } catch (EventConstructionException e) {
+ Logger.getGlobal().log(Level.SEVERE, "Could not create Event.", e);
+ throw new RuntimeException(e);
}
}
@@ -80,7 +78,6 @@ public class HostController extends Controller {
*/
public void hostGame(){
hostWrapper.setVisible(true);
- System.out.println("Reacted hostGame");
}
}
diff --git a/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java b/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java
index dc4d8e59..5ae51d35 100644
--- a/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java
+++ b/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java
@@ -58,7 +58,6 @@ public class LobbyController extends Controller {
}
else {
joinGameBtn.setDisable(true);
- System.out.println(curr.statusProperty().getValue());
}
});
joinGameBtn.setDisable(true);
@@ -70,11 +69,6 @@ public class LobbyController extends Controller {
public void refreshBtnPressed(){
for(RaceConnection connection: connections) {
connection.check();
- try {
- Thread.sleep(500);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
}
try {
if (lobbyTable.getSelectionModel().getSelectedItem().statusProperty().getValue().equals("Ready")) {
diff --git a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java
index 80ef2da4..7cdd0e73 100644
--- a/racevisionGame/src/main/java/visualiser/Controllers/MainController.java
+++ b/racevisionGame/src/main/java/visualiser/Controllers/MainController.java
@@ -3,15 +3,15 @@ package visualiser.Controllers;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.layout.AnchorPane;
-import visualiser.app.VisualiserInput;
import visualiser.gameController.ControllerClient;
import visualiser.model.VisualiserBoat;
-import visualiser.model.VisualiserRace;
+import visualiser.model.VisualiserRaceEvent;
import java.net.Socket;
import java.net.URL;
import java.util.ResourceBundle;
+
/**
* Controller that everything is overlayed onto. This makes it so that changing scenes does not resize your stage.
*/
@@ -36,12 +36,11 @@ public class MainController extends Controller {
/**
* Transitions from the StartController screen (displays pre-race information) to the RaceController (displays the actual race).
- * @param visualiserInput The object used to read packets from the race server.
* @param visualiserRace The object modelling the race.
* @param controllerClient Socket Client that manipulates the controller.
*/
- public void beginRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace, ControllerClient controllerClient) {
- raceController.startRace(visualiserInput, visualiserRace, controllerClient);
+ public void beginRace(VisualiserRaceEvent visualiserRace, ControllerClient controllerClient) {
+ raceController.startRace(visualiserRace, controllerClient);
}
/**
@@ -103,10 +102,15 @@ public class MainController extends Controller {
AnchorPane.setLeftAnchor(startController.startWrapper(), 0.0);
AnchorPane.setRightAnchor(startController.startWrapper(), 0.0);
- AnchorPane.setTopAnchor(connectionController.startWrapper(), 0.0);
- AnchorPane.setBottomAnchor(connectionController.startWrapper(), 0.0);
- AnchorPane.setLeftAnchor(connectionController.startWrapper(), 0.0);
- AnchorPane.setRightAnchor(connectionController.startWrapper(), 0.0);
+ AnchorPane.setTopAnchor(lobbyController.startWrapper(), 0.0);
+ AnchorPane.setBottomAnchor(lobbyController.startWrapper(), 0.0);
+ AnchorPane.setLeftAnchor(lobbyController.startWrapper(), 0.0);
+ AnchorPane.setRightAnchor(lobbyController.startWrapper(), 0.0);
+
+ AnchorPane.setTopAnchor(hostController.startWrapper(), 0.0);
+ AnchorPane.setBottomAnchor(hostController.startWrapper(), 0.0);
+ AnchorPane.setLeftAnchor(hostController.startWrapper(), 0.0);
+ AnchorPane.setRightAnchor(hostController.startWrapper(), 0.0);
AnchorPane.setTopAnchor(finishController.finishWrapper, 0.0);
AnchorPane.setBottomAnchor(finishController.finishWrapper, 0.0);
diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java
index 5777c06e..3e4c1398 100644
--- a/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java
+++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceController.java
@@ -4,8 +4,9 @@ package visualiser.Controllers;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
-import javafx.event.EventHandler;
+import javafx.collections.transformation.SortedList;
import javafx.fxml.FXML;
import javafx.scene.chart.LineChart;
import javafx.scene.control.*;
@@ -18,37 +19,38 @@ import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import network.Messages.Enums.RaceStatusEnum;
import shared.model.Leg;
-import visualiser.app.VisualiserInput;
import visualiser.gameController.ControllerClient;
import visualiser.gameController.Keys.ControlKey;
import visualiser.gameController.Keys.KeyFactory;
import visualiser.model.*;
+import visualiser.network.ServerConnection;
-import java.awt.*;
-import java.io.IOException;
import java.net.URL;
-import java.text.DecimalFormat;
import java.util.ResourceBundle;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
/**
* Controller used to display a running race.
*/
public class RaceController extends Controller {
- /**
- * The object used to read packets from the connected server.
- */
- private VisualiserInput visualiserInput;
/**
* The race object which describes the currently occurring race.
*/
- private VisualiserRace visualiserRace;
+ private VisualiserRaceEvent visualiserRace;
+
/**
- * An additional observable list of boats. This is used by the table view, to allow it to sort boats without effecting the race's own list of boats.
+ * Service for sending keystrokes to server
*/
- private ObservableList tableBoatList;
+ private ControllerClient controllerClient;
+
+
+
/**
* The canvas that draws the race.
@@ -61,14 +63,25 @@ public class RaceController extends Controller {
private Sparkline sparkline;
/**
- * Service for sending keystrokes to server
+ * The arrow controller.
*/
- private ControllerClient controllerClient;
+ @FXML private ArrowController arrowController;
+
@FXML private GridPane canvasBase;
- @FXML private Pane arrow;
+
+
@FXML private SplitPane race;
+
+ /**
+ * This is the root node of the arrow control.
+ */
+ @FXML private Pane arrow;
+
+ /**
+ * This is the pane we place the actual arrow control inside of.
+ */
@FXML private StackPane arrowPane;
@FXML private Label timer;
@FXML private Label FPS;
@@ -103,8 +116,9 @@ public class RaceController extends Controller {
controllerClient.sendKey(controlKey);
controlKey.onAction(); // Change key state if applicable
event.consume();
- } catch (IOException e) {
- e.printStackTrace();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Logger.getGlobal().log(Level.WARNING, "RaceController was interrupted on thread: " + Thread.currentThread() + "while sending: " + controlKey, e);
}
}
});
@@ -118,15 +132,15 @@ public class RaceController extends Controller {
//Fps display.
initialiseFps(this.visualiserRace);
- //Need to add the included arrow pane to the arrowPane container.
- initialiseArrow();
-
//Information table.
initialiseInfoTable(this.visualiserRace);
//Sparkline.
initialiseSparkline(this.visualiserRace);
+ //Arrow.
+ initialiseArrow(this.visualiserRace);
+
//Race canvas.
initialiseRaceCanvas(this.visualiserRace);
@@ -147,7 +161,7 @@ public class RaceController extends Controller {
* Initialises the frame rate functionality. This allows for toggling the frame rate, and connect the fps label to the race's fps property.
* @param visualiserRace The race to connect the fps label to.
*/
- private void initialiseFps(VisualiserRace visualiserRace) {
+ private void initialiseFps(VisualiserRaceEvent visualiserRace) {
//On/off toggle.
initialiseFpsToggle();
@@ -177,9 +191,9 @@ public class RaceController extends Controller {
* Initialises the fps label to update when the race fps changes.
* @param visualiserRace The race to monitor the frame rate of.
*/
- private void initialiseFpsLabel(VisualiserRace visualiserRace) {
+ private void initialiseFpsLabel(VisualiserRaceEvent visualiserRace) {
- visualiserRace.fpsProperty().addListener((observable, oldValue, newValue) -> {
+ visualiserRace.getFrameRateProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> this.FPS.setText("FPS: " + newValue.toString()));
});
@@ -191,14 +205,21 @@ public class RaceController extends Controller {
* Initialises the information table view to listen to a given race.
* @param race Race to listen to.
*/
- public void initialiseInfoTable(VisualiserRace race) {
+ public void initialiseInfoTable(VisualiserRaceEvent race) {
//Copy list of boats.
- this.tableBoatList = FXCollections.observableArrayList(race.getBoats());
+ ObservableList boats = FXCollections.observableArrayList(race.getVisualiserRaceState().getBoats());
+ SortedList sortedBoats = new SortedList<>(boats);
+ sortedBoats.comparatorProperty().bind(boatInfoTable.comparatorProperty());
+
+ //Update copy when original changes.
+ race.getVisualiserRaceState().getBoats().addListener((ListChangeListener.Change extends VisualiserBoat> c) -> Platform.runLater(() -> {
+ boats.setAll(race.getVisualiserRaceState().getBoats());
+ }));
//Set up table.
- boatInfoTable.setItems(this.tableBoatList);
+ boatInfoTable.setItems(sortedBoats);
//Set up each column.
@@ -278,12 +299,12 @@ public class RaceController extends Controller {
/**
- * Initialises the {@link Sparkline}, and listens to a specified {@link VisualiserRace}.
+ * Initialises the {@link Sparkline}, and listens to a specified {@link VisualiserRaceEvent}.
* @param race The race to listen to.
*/
- private void initialiseSparkline(VisualiserRace race) {
+ private void initialiseSparkline(VisualiserRaceEvent race) {
//The race.getBoats() we are passing in is sorted by position in race inside the race class.
- this.sparkline = new Sparkline(this.visualiserRace, this.sparklineChart);
+ this.sparkline = new Sparkline(this.visualiserRace.getVisualiserRaceState(), this.sparklineChart);
}
@@ -291,10 +312,10 @@ public class RaceController extends Controller {
* Initialises the {@link ResizableRaceCanvas}, provides the race to read data from.
* @param race Race to read data from.
*/
- private void initialiseRaceCanvas(VisualiserRace race) {
+ private void initialiseRaceCanvas(VisualiserRaceEvent race) {
//Create canvas.
- raceCanvas = new ResizableRaceCanvas(race, arrow.getChildren().get(0));
+ raceCanvas = new ResizableRaceCanvas(race);
//Set properties.
raceCanvas.setMouseTransparent(true);
@@ -314,18 +335,18 @@ public class RaceController extends Controller {
* Intialises the race time zone label with the race's time zone.
* @param race The race to get time zone from.
*/
- private void initialiseRaceTimezoneLabel(VisualiserRace race) {
- timeZone.setText(race.getRaceClock().getTimeZone());
+ private void initialiseRaceTimezoneLabel(VisualiserRaceEvent race) {
+ timeZone.setText(race.getVisualiserRaceState().getRaceClock().getTimeZone());
}
/**
* Initialises the race clock to listen to the specified race.
* @param race The race to listen to.
*/
- private void initialiseRaceClock(VisualiserRace race) {
+ private void initialiseRaceClock(VisualiserRaceEvent race) {
//RaceClock.duration isn't necessarily being changed in the javaFX thread, so we need to runlater the update.
- race.getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> {
+ race.getVisualiserRaceState().getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
timer.setText(newValue);
});
@@ -336,13 +357,11 @@ public class RaceController extends Controller {
/**
* Displays a specified race.
- * @param visualiserInput Object used to read packets from server.
* @param visualiserRace Object modelling the race.
* @param controllerClient Socket Client that manipulates the controller.
*/
- public void startRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace, ControllerClient controllerClient) {
+ public void startRace(VisualiserRaceEvent visualiserRace, ControllerClient controllerClient) {
- this.visualiserInput = visualiserInput;
this.visualiserRace = visualiserRace;
this.controllerClient = controllerClient;
@@ -360,17 +379,18 @@ public class RaceController extends Controller {
* Transition from the race view to the finish view.
* @param boats boats there are in the race.
*/
- public void finishRace(ObservableList boats){
+ public void finishRace(ObservableList boats) {
race.setVisible(false);
parent.enterFinish(boats);
}
/**
- * Adds the included arrow pane (see arrow.fxml) to the arrowPane (see race.fxml).
+ * Initialises the arrow controller with data from the race to observe.
+ * @param race The race to observe.
*/
- private void initialiseArrow() {
- arrowPane.getChildren().add(arrow);
+ private void initialiseArrow(VisualiserRaceEvent race) {
+ arrowController.setWindProperty(race.getVisualiserRaceState().windProperty());
}
@@ -383,7 +403,7 @@ public class RaceController extends Controller {
public void handle(long arg0) {
//Get the current race status.
- RaceStatusEnum raceStatus = visualiserRace.getRaceStatusEnum();
+ RaceStatusEnum raceStatus = visualiserRace.getVisualiserRaceState().getRaceStatusEnum();
//If the race has finished, go to finish view.
@@ -392,7 +412,7 @@ public class RaceController extends Controller {
stop();
//Hide this, and display the finish controller.
- finishRace(visualiserRace.getBoats());
+ finishRace(visualiserRace.getVisualiserRaceState().getBoats());
} else {
//Otherwise, render the canvas.
@@ -404,6 +424,15 @@ public class RaceController extends Controller {
}
+ //Return to main screen if we lose connection.
+ if (!visualiserRace.getServerConnection().isAlive()) {
+ race.setVisible(false);
+ parent.enterTitle();
+ //TODO currently this doesn't work correctly - the title screen remains visible after clicking join game
+ //TODO we should display an error to the user
+ //TODO also need to "reset" any state (race, connections, etc...).
+ }
+
}
}.start();
}
diff --git a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java
index 8db4ec60..b2d6b2b7 100644
--- a/racevisionGame/src/main/java/visualiser/Controllers/StartController.java
+++ b/racevisionGame/src/main/java/visualiser/Controllers/StartController.java
@@ -9,8 +9,9 @@ import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
-import javafx.scene.paint.Color;
+import mock.model.commandFactory.CompositeCommand;
import network.Messages.Enums.RaceStatusEnum;
+import network.Messages.Enums.RequestToJoinEnum;
import network.Messages.LatestMessages;
import shared.dataInput.*;
import shared.enums.XMLFileType;
@@ -18,20 +19,24 @@ import shared.exceptions.InvalidBoatDataException;
import shared.exceptions.InvalidRaceDataException;
import shared.exceptions.InvalidRegattaDataException;
import shared.exceptions.XMLReaderException;
-import visualiser.app.VisualiserInput;
import visualiser.gameController.ControllerClient;
+import visualiser.model.VisualiserRaceState;
+import visualiser.network.ServerConnection;
import visualiser.model.VisualiserBoat;
-import visualiser.model.VisualiserRace;
+import visualiser.model.VisualiserRaceEvent;
import java.io.IOException;
import java.net.Socket;
import java.net.URL;
import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
/**
* Controller to for waiting for the race to start.
*/
-public class StartController extends Controller implements Observer {
+public class StartController extends Controller {
@FXML private GridPane start;
@FXML private AnchorPane startWrapper;
@@ -66,33 +71,21 @@ public class StartController extends Controller implements Observer {
@FXML private Label raceStatusLabel;
-
/**
- * The object used to read packets from the connected server.
+ * The race + connection to server.
*/
- private VisualiserInput visualiserInput;
+ private VisualiserRaceEvent visualiserRaceEvent;
+
/**
- * The race object which describes the currently occurring race.
+ * Writes BoatActions to outgoing message queue.
*/
- private VisualiserRace visualiserRace;
-
private ControllerClient controllerClient;
- /**
- * An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor.
- */
- List colors = new ArrayList<>(Arrays.asList(
- Color.BLUEVIOLET,
- Color.BLACK,
- Color.RED,
- Color.ORANGE,
- Color.DARKOLIVEGREEN,
- Color.LIMEGREEN,
- Color.PURPLE,
- Color.DARKGRAY,
- Color.YELLOW
- ));
+
+
+
+
@@ -109,33 +102,17 @@ public class StartController extends Controller implements Observer {
/**
* Starts the race.
- * Called once we have received all XML files from the server.
- * @param latestMessages The set of latest race messages to use for race.
- * @throws XMLReaderException Thrown if XML file cannot be parsed.
- * @throws InvalidRaceDataException Thrown if XML file cannot be parsed.
- * @throws InvalidBoatDataException Thrown if XML file cannot be parsed.
- * @throws InvalidRegattaDataException Thrown if XML file cannot be parsed.
*/
- private void startRace(LatestMessages latestMessages) throws XMLReaderException, InvalidRaceDataException, InvalidBoatDataException, InvalidRegattaDataException {
-
- //Create data sources from latest messages for the race.
- RaceDataSource raceDataSource = new RaceXMLReader(latestMessages.getRaceXMLMessage().getXmlMessage(), XMLFileType.Contents);
- BoatDataSource boatDataSource = new BoatXMLReader(latestMessages.getBoatXMLMessage().getXmlMessage(), XMLFileType.Contents);
- RegattaDataSource regattaDataSource = new RegattaXMLReader(latestMessages.getRegattaXMLMessage().getXmlMessage(), XMLFileType.Contents);
-
- //Create race.
- this.visualiserRace = new VisualiserRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, this.colors);
- new Thread(this.visualiserRace).start();
-
+ private void startRace() {
//Initialise the boat table.
- initialiseBoatTable(this.visualiserRace);
+ initialiseBoatTable(this.visualiserRaceEvent.getVisualiserRaceState());
//Initialise the race name.
- initialiseRaceName(this.visualiserRace);
+ initialiseRaceName(this.visualiserRaceEvent.getVisualiserRaceState());
//Initialises the race clock.
- initialiseRaceClock(this.visualiserRace);
+ initialiseRaceClock(this.visualiserRaceEvent.getVisualiserRaceState());
//Starts the race countdown timer.
countdownTimer();
@@ -153,7 +130,7 @@ public class StartController extends Controller implements Observer {
* Initialises the boat table that is to be shown on the pane.
* @param visualiserRace The race to get data from.
*/
- private void initialiseBoatTable(VisualiserRace visualiserRace) {
+ private void initialiseBoatTable(VisualiserRaceState visualiserRace) {
//Get the boats.
ObservableList boats = visualiserRace.getBoats();
@@ -168,7 +145,7 @@ public class StartController extends Controller implements Observer {
* Initialises the race name which is shown on the pane.
* @param visualiserRace The race to get data from.
*/
- private void initialiseRaceName(VisualiserRace visualiserRace) {
+ private void initialiseRaceName(VisualiserRaceState visualiserRace) {
raceTitleLabel.setText(visualiserRace.getRegattaName());
@@ -178,7 +155,7 @@ public class StartController extends Controller implements Observer {
* Initialises the race clock/timer labels for the start time, current time, and remaining time.
* @param visualiserRace The race to get data from.
*/
- private void initialiseRaceClock(VisualiserRace visualiserRace) {
+ private void initialiseRaceClock(VisualiserRaceState visualiserRace) {
//Start time.
initialiseRaceClockStartTime(visualiserRace);
@@ -196,7 +173,7 @@ public class StartController extends Controller implements Observer {
* Initialises the race current time label.
* @param visualiserRace The race to get data from.
*/
- private void initialiseRaceClockStartTime(VisualiserRace visualiserRace) {
+ private void initialiseRaceClockStartTime(VisualiserRaceState visualiserRace) {
raceStartLabel.setText(visualiserRace.getRaceClock().getStartingTimeString());
@@ -213,7 +190,7 @@ public class StartController extends Controller implements Observer {
* Initialises the race current time label.
* @param visualiserRace The race to get data from.
*/
- private void initialiseRaceClockCurrentTime(VisualiserRace visualiserRace) {
+ private void initialiseRaceClockCurrentTime(VisualiserRaceState visualiserRace) {
visualiserRace.getRaceClock().currentTimeProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
@@ -227,7 +204,7 @@ public class StartController extends Controller implements Observer {
* Initialises the race duration label.
* @param visualiserRace The race to get data from.
*/
- private void initialiseRaceClockDuration(VisualiserRace visualiserRace) {
+ private void initialiseRaceClockDuration(VisualiserRaceState visualiserRace) {
visualiserRace.getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
@@ -245,13 +222,13 @@ public class StartController extends Controller implements Observer {
@Override
public void handle(long arg0) {
- //TODO instead of having an AnimationTimer checking the race status, we could provide a Property, and connect a listener to that.
//Get the current race status.
- RaceStatusEnum raceStatus = visualiserRace.getRaceStatusEnum();
+ RaceStatusEnum raceStatus = visualiserRaceEvent.getVisualiserRaceState().getRaceStatusEnum();
//Display it.
raceStatusLabel.setText("Race Status: " + raceStatus.name());
+
//If the race has reached the preparatory phase, or has started...
if (raceStatus == RaceStatusEnum.PREPARATORY || raceStatus == RaceStatusEnum.STARTED) {
//Stop this timer.
@@ -259,9 +236,10 @@ public class StartController extends Controller implements Observer {
//Hide this, and display the race controller.
startWrapper.setVisible(false);
- start.setVisible(false);
+ //start.setVisible(false);//TODO is this needed?
+
+ parent.beginRace(visualiserRaceEvent, controllerClient);
- parent.beginRace(visualiserInput, visualiserRace, controllerClient);
}
}
}.start();
@@ -270,56 +248,24 @@ public class StartController extends Controller implements Observer {
/**
- * Function to handle changes in objects we observe.
- * We observe LatestMessages.
- * @param o The observed object.
- * @param arg The {@link Observable#notifyObservers(Object)} parameter.
+ * Show starting information for a race given a socket.
+ * @param socket network source of information
*/
- @Override
- public void update(Observable o, Object arg) {
-
- //Check that we actually have LatestMessages.
- if (o instanceof LatestMessages) {
- LatestMessages latestMessages = (LatestMessages) o;
+ public void enterLobby(Socket socket) {
+ try {
- //If we've received all of the xml files, start the race. Only start it if it hasn't already been created.
- if (latestMessages.hasAllXMLMessages() && this.visualiserRace == null) {
+ this.visualiserRaceEvent = new VisualiserRaceEvent(socket, RequestToJoinEnum.PARTICIPANT);
- //Need to handle it in the javafx thread.
- Platform.runLater(() -> {
- try {
- this.startRace(latestMessages);
+ this.controllerClient = visualiserRaceEvent.getControllerClient();
- } catch (XMLReaderException | InvalidBoatDataException | InvalidRaceDataException | InvalidRegattaDataException e) {
- //We currently don't handle this in meaningful way, as it should never occur.
- //If we reach this point it means that malformed XML files were sent.
- e.printStackTrace();
- }
- });
- }
- }
+ startWrapper.setVisible(true);
- }
-
- /**
- * Show starting information for a race given a socket.
- * @param socket network source of information
- */
- public void enterLobby(Socket socket) {
- startWrapper.setVisible(true);
- try {
- //Begin reading packets from the socket/server.
- this.visualiserInput = new VisualiserInput(socket);
- //Send controller input to server
- this.controllerClient = new ControllerClient(socket);
- //Store a reference to latestMessages so that we can observe it.
- LatestMessages latestMessages = this.visualiserInput.getLatestMessages();
- latestMessages.addObserver(this);
- new Thread(this.visualiserInput).start();
+ startRace();
} catch (IOException e) {
- e.printStackTrace();
+ //TODO should probably let this propagate, so that we only enter this scene if everything works
+ Logger.getGlobal().log(Level.WARNING, "Could not connect to server.", e);
}
}
diff --git a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java b/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java
deleted file mode 100644
index 0b8102b5..00000000
--- a/racevisionGame/src/main/java/visualiser/app/VisualiserInput.java
+++ /dev/null
@@ -1,376 +0,0 @@
-package visualiser.app;
-import javafx.application.Platform;
-import network.BinaryMessageDecoder;
-import network.Exceptions.InvalidMessageException;
-import network.Messages.*;
-import org.xml.sax.SAXException;
-import shared.dataInput.BoatXMLReader;
-import shared.dataInput.RaceXMLReader;
-import shared.dataInput.RegattaXMLReader;
-import shared.exceptions.InvalidBoatDataException;
-import shared.exceptions.InvalidRaceDataException;
-import shared.exceptions.InvalidRegattaDataException;
-import shared.exceptions.XMLReaderException;
-
-import javax.xml.parsers.ParserConfigurationException;
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.net.Socket;
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ArrayBlockingQueue;
-
-import static network.Utils.ByteConverter.bytesToShort;
-
-/**
- * TCP client which receives packets/messages from a race data source
- * (e.g., mock source, official source), and exposes them to any observers.
- */
-public class VisualiserInput implements Runnable {
-
- /**
- * Timestamp of the last heartbeat.
- */
- private long lastHeartbeatTime = -1;
- /**
- * Sequence number of the last heartbeat.
- */
- private long lastHeartbeatSequenceNum = -1;
-
-
- /**
- * The socket that we have connected to.
- */
- private Socket connectionSocket;
-
-
- /**
- * InputStream (from the socket).
- */
- private DataInputStream inStream;
-
-
- /**
- * An object containing the set of latest messages to write to.
- * Every server frame, VisualiserInput reads messages from its inputStream, and write them to this.
- */
- private LatestMessages latestMessages;
-
-
-
- /**
- * Ctor.
- * @param socket Socket from which we will receive race data.
- * @throws IOException If there is something wrong with the socket's input stream.
- */
- public VisualiserInput(Socket socket) throws IOException {
-
- this.connectionSocket = socket;
-
- //We wrap a DataInputStream around the socket's InputStream because it has the stream.readFully(buffer) function, which is a blocking read until the buffer has been filled.
- this.inStream = new DataInputStream(connectionSocket.getInputStream());
-
- this.latestMessages = new LatestMessages();
-
-
- this.lastHeartbeatTime = System.currentTimeMillis();
- }
-
-
- /**
- * Returns the LatestMessages object, which can be queried for any received race related messages.
- * @return The LatestMessages object.
- */
- public LatestMessages getLatestMessages() {
- return latestMessages;
- }
-
- /**
- * Calculates the time since last heartbeat, in milliseconds.
- * @return Time since last heartbeat, in milliseconds..
- */
- private double timeSinceHeartbeat() {
- long now = System.currentTimeMillis();
- return (now - lastHeartbeatTime);
- }
-
-
-
- /**
- * Reads and returns the next message as an array of bytes from the socket. Use getNextMessage() to get the actual message object instead.
- * @return Encoded binary message bytes.
- * @throws IOException Thrown when an error occurs while reading from the socket.
- */
- private byte[] getNextMessageBytes() throws IOException {
- inStream.mark(0);
- short CRCLength = 4;
- short headerLength = 15;
-
- //Read the header of the next message.
- byte[] headerBytes = new byte[headerLength];
- inStream.readFully(headerBytes);
-
- //Read the message body length.
- byte[] messageBodyLengthBytes = Arrays.copyOfRange(headerBytes, headerLength - 2, headerLength);
- short messageBodyLength = bytesToShort(messageBodyLengthBytes);
-
- //Read the message body.
- byte[] messageBodyBytes = new byte[messageBodyLength];
- inStream.readFully(messageBodyBytes);
-
- //Read the message CRC.
- byte[] messageCRCBytes = new byte[CRCLength];
- inStream.readFully(messageCRCBytes);
-
- //Put the head + body + crc into one large array.
- ByteBuffer messageBytes = ByteBuffer.allocate(headerBytes.length + messageBodyBytes.length + messageCRCBytes.length);
- messageBytes.put(headerBytes);
- messageBytes.put(messageBodyBytes);
- messageBytes.put(messageCRCBytes);
-
- return messageBytes.array();
- }
-
- /**
- * Reads and returns the next message object from the socket.
- * @return The message object. Use instanceof for concrete type.
- * @throws IOException Thrown when an error occurs while reading from the socket.
- * @throws InvalidMessageException Thrown when the message is invalid in some way.
- */
- private AC35Data getNextMessage() throws IOException, InvalidMessageException
- {
- //Get the next message from the socket as a block of bytes.
- byte[] messageBytes = this.getNextMessageBytes();
-
- //Decode the binary message into an appropriate message object.
- BinaryMessageDecoder decoder = new BinaryMessageDecoder(messageBytes);
-
- return decoder.decode();
-
- }
-
- /**
- * Main loop which reads messages from the socket, and exposes them.
- */
- public void run(){
- boolean receiverLoop = true;
- //receiver loop that gets the input
- while (receiverLoop) {
-
- //If no heartbeat has been received in more the heartbeat period
- //then the connection will need to be restarted.
- //System.out.println("time since last heartbeat: " + timeSinceHeartbeat());//TEMP REMOVE
- long heartBeatPeriod = 10 * 1000;
- if (timeSinceHeartbeat() > heartBeatPeriod) {
- System.out.println("Connection has stopped, trying to reconnect.");
-
- //Attempt to reconnect the socket.
- try {//This attempt doesn't really work. Under what circumstances would
- this.connectionSocket = new Socket(this.connectionSocket.getInetAddress(), this.connectionSocket.getPort());
- //this.connectionSocket.connect(this.connectionSocket.getRemoteSocketAddress());
- //Reset the heartbeat timer.
- this.lastHeartbeatTime = System.currentTimeMillis();
- }
- catch (IOException e) {
- System.err.println("Unable to reconnect.");
-
- //Wait 500ms. Ugly hack, should refactor.
- long waitPeriod = 500;
- long waitTimeStart = System.currentTimeMillis() + waitPeriod;
-
- while (System.currentTimeMillis() < waitTimeStart){
- //Nothing. Busyloop.
- }
-
- //Swallow the exception.
- continue;
- }
-
- }
-
- //Reads the next message.
- AC35Data message;
- try {
- message = this.getNextMessage();
- }
- catch (InvalidMessageException | IOException e) {
- //Prints exception to stderr, and iterate loop (that is, read the next message).
- System.err.println("Unable to read message: " + e.getMessage());
- try {
- inStream.reset();
- } catch (IOException e1) {
- e1.printStackTrace();
- }
- //Continue to the next loop iteration/message.
- continue;
- }
-
-
- //Checks which message is being received and does what is needed for that message.
- switch (message.getType()) {
-
- //Heartbeat.
- case HEARTBEAT: {
- Heartbeat heartbeat = (Heartbeat) message;
-
- //Check that the heartbeat number is greater than the previous value, and then set the last heartbeat time.
- if (heartbeat.getSequenceNumber() > this.lastHeartbeatSequenceNum) {
- lastHeartbeatTime = System.currentTimeMillis();
- lastHeartbeatSequenceNum = heartbeat.getSequenceNumber();
- //System.out.println("HeartBeat Message! " + lastHeartbeatSequenceNum);
- }
-
- break;
- }
-
- //RaceStatus.
- case RACESTATUS: {
- RaceStatus raceStatus = (RaceStatus) message;
-
- //System.out.println("Race Status Message");
- this.latestMessages.setRaceStatus(raceStatus);
-
- for (BoatStatus boatStatus : raceStatus.getBoatStatuses()) {
- this.latestMessages.setBoatStatus(boatStatus);
- }
-
- break;
- }
-
- //DisplayTextMessage.
- case DISPLAYTEXTMESSAGE: {
- //System.out.println("Display Text Message");
- //No decoder for this.
-
- break;
- }
-
- //XMLMessage.
- case XMLMESSAGE: {
- XMLMessage xmlMessage = (XMLMessage) message;
-
- //System.out.println("XML Message!");
-
- this.latestMessages.setXMLMessage(xmlMessage);
-
- break;
- }
-
- //RaceStartStatus.
- case RACESTARTSTATUS: {
-
- //System.out.println("Race Start Status Message");
-
- break;
- }
-
- //YachtEventCode.
- case YACHTEVENTCODE: {
- //YachtEventCode yachtEventCode = (YachtEventCode) message;
-
- //System.out.println("Yacht Event Code!");
- //No decoder for this.
-
- break;
- }
-
- //YachtActionCode.
- case YACHTACTIONCODE: {
- //YachtActionCode yachtActionCode = (YachtActionCode) message;
-
- //System.out.println("Yacht Action Code!");
- // No decoder for this.
-
- break;
- }
-
- //ChatterText.
- case CHATTERTEXT: {
- //ChatterText chatterText = (ChatterText) message;
-
- //System.out.println("Chatter Text Message!");
- //No decoder for this.
-
- break;
- }
-
- //BoatLocation.
- case BOATLOCATION: {
- BoatLocation boatLocation = (BoatLocation) message;
-
- //System.out.println("Boat Location!");
-
- BoatLocation existingBoatLocation = this.latestMessages.getBoatLocationMap().get(boatLocation.getSourceID());
- if (existingBoatLocation != null) {
- //If our boatlocation map already contains a boat location message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
- if (boatLocation.getTime() > existingBoatLocation.getTime()) {
- //If it is, replace the old message.
- this.latestMessages.setBoatLocation(boatLocation);
- }
- } else {
- //If the map _doesn't_ already contain a message for this boat, insert the message.
- this.latestMessages.setBoatLocation(boatLocation);
- }
-
- break;
- }
-
- //MarkRounding.
- case MARKROUNDING: {
- MarkRounding markRounding = (MarkRounding) message;
-
- //System.out.println("Mark Rounding Message!");
-
- MarkRounding existingMarkRounding = this.latestMessages.getMarkRoundingMap().get(markRounding.getSourceID());
- if (existingMarkRounding != null) {
-
- //If our markRoundingMap already contains a mark rounding message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
- if (markRounding.getTime() > existingMarkRounding.getTime()) {
- //If it is, replace the old message.
- this.latestMessages.setMarkRounding(markRounding);
- }
-
- } else {
- //If the map _doesn't_ already contain a message for this boat, insert the message.
- this.latestMessages.setMarkRounding(markRounding);
- }
-
- break;
- }
-
- //CourseWinds.
- case COURSEWIND: {
-
- //System.out.println("Course Wind Message!");
- CourseWinds courseWinds = (CourseWinds) message;
-
- this.latestMessages.setCourseWinds(courseWinds);
-
- break;
- }
-
- //AverageWind.
- case AVGWIND: {
-
- //System.out.println("Average Wind Message!");
- AverageWind averageWind = (AverageWind) message;
-
- this.latestMessages.setAverageWind(averageWind);
-
- break;
- }
-
- //Unrecognised message.
- default: {
- System.out.println("Broken Message!");
-
- break;
- }
-
- }
- }
- }
-
-}
diff --git a/racevisionGame/src/main/java/visualiser/enums/ConnectionToServerState.java b/racevisionGame/src/main/java/visualiser/enums/ConnectionToServerState.java
new file mode 100644
index 00000000..22e1e30f
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/enums/ConnectionToServerState.java
@@ -0,0 +1,96 @@
+package visualiser.enums;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The states in which a connection from a client to a server may have.
+ */
+public enum ConnectionToServerState {
+
+ UNKNOWN(0),
+
+ /**
+ * We're waiting for the server to complete the joining handshake.
+ * See {@link network.Messages.RequestToJoin} and {@link network.Messages.JoinAcceptance}.
+ */
+ REQUEST_SENT(1),
+
+ /**
+ * The client has receved a {@link network.Messages.JoinAcceptance} from the server.
+ */
+ RESPONSE_RECEIVED(2),
+
+ /**
+ * The server has completed the handshake, and is connected.
+ * That is, the client sent a {@link network.Messages.RequestToJoin}, which was successful, and the server responded with a {@link network.Messages.JoinAcceptance}.
+ */
+ CONNECTED(3),
+
+ /**
+ * The server has timed out, or the connection has been interrupted.
+ */
+ TIMED_OUT(4),
+
+ /**
+ * The client's connection has been declined.
+ */
+ DECLINED(5);
+
+
+
+
+ private byte value;
+
+ /**
+ * Ctor. Creates a ConnectionToServerState from a given primitive integer value, cast to a byte.
+ * @param value Integer, which is cast to byte, to construct from.
+ */
+ private ConnectionToServerState(int value) {
+ this.value = (byte) value;
+ }
+
+ /**
+ * Returns the primitive value of the enum.
+ * @return Primitive value of the enum.
+ */
+ public byte getValue() {
+ return value;
+ }
+
+
+ /**
+ * Stores a mapping between Byte values and ConnectionToServerState values.
+ */
+ private static final Map byteToStateMap = new HashMap<>();
+
+
+ /*
+ Static initialization block. Initializes the byteToStateMap.
+ */
+ static {
+ for (ConnectionToServerState type : ConnectionToServerState.values()) {
+ ConnectionToServerState.byteToStateMap.put(type.value, type);
+ }
+ }
+
+
+ /**
+ * Returns the enumeration value which corresponds to a given byte value.
+ * @param connectionState Byte value to convert to a ConnectionToServerState value.
+ * @return The ConnectionToServerState value which corresponds to the given byte value.
+ */
+ public static ConnectionToServerState fromByte(byte connectionState) {
+ //Gets the corresponding ConnectionToServerState from the map.
+ ConnectionToServerState type = ConnectionToServerState.byteToStateMap.get(connectionState);
+
+ if (type == null) {
+ //If the byte value wasn't found, return the UNKNOWN ConnectionToServerState.
+ return ConnectionToServerState.UNKNOWN;
+ } else {
+ //Otherwise, return the ConnectionToServerState.
+ return type;
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java b/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java
index a0f1af75..57459868 100644
--- a/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java
+++ b/racevisionGame/src/main/java/visualiser/gameController/ControllerClient.java
@@ -1,7 +1,9 @@
package visualiser.gameController;
import network.BinaryMessageEncoder;
+import network.Exceptions.InvalidMessageException;
import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Messages.AC35Data;
import network.Messages.BoatAction;
import network.Messages.Enums.BoatActionEnum;
import network.Messages.Enums.MessageType;
@@ -12,57 +14,41 @@ import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.nio.ByteBuffer;
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
/**
* Basic service for sending key presses to game server
*/
public class ControllerClient {
- /**
- * Socket to server
- */
- Socket socket;
/**
- * Output stream wrapper for socket to server
+ * Queue of messages to be sent to server.
*/
- DataOutputStream outputStream;
+ private BlockingQueue outgoingMessages;
/**
* Initialise controller client with live socket.
- * @param socket to server
+ * @param outgoingMessages Queue to place messages on to send to server.
*/
- public ControllerClient(Socket socket) {
- this.socket = socket;
-
- try {
- this.outputStream = new DataOutputStream(socket.getOutputStream());
- } catch (IOException e) {
- e.printStackTrace();
- }
+ public ControllerClient(BlockingQueue outgoingMessages) {
+ this.outgoingMessages = outgoingMessages;
}
/**
* Send a keypress to server
* @param key to send
- * @throws IOException if socket write fails
+ * @throws InterruptedException If thread is interrupted while writing message to outgoing message queue.
*/
- public void sendKey(ControlKey key) throws IOException {
+ public void sendKey(ControlKey key) throws InterruptedException {
BoatActionEnum protocolCode = key.getProtocolCode();
if(protocolCode != BoatActionEnum.NOT_A_STATUS) {
- byte[] bytes = new byte[4];
- ByteBuffer.wrap(bytes).putInt(protocolCode.getValue());
- BoatActionEnum boatActionEnum = BoatActionEnum.fromByte(bytes[3]);
-
- BoatAction boatAction = new BoatAction(boatActionEnum);
-
- byte[] encodedBoatAction = RaceVisionByteEncoder.boatActionMessage(boatAction);
+ BoatAction boatAction = new BoatAction(protocolCode);
- BinaryMessageEncoder binaryMessage = new BinaryMessageEncoder(MessageType.BOATACTION, System.currentTimeMillis(), 0,
- (short) encodedBoatAction.length, encodedBoatAction);
+ outgoingMessages.put(boatAction);
- System.out.println("Sending out key: " + boatActionEnum);
- outputStream.write(binaryMessage.getFullMessage());
}
}
}
diff --git a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java
index 66e98d89..b9dde4af 100644
--- a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java
+++ b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java
@@ -1,76 +1,102 @@
package visualiser.gameController;
-import mock.model.RaceLogic;
-import network.BinaryMessageDecoder;
-import network.MessageDecoders.BoatActionDecoder;
-import network.Messages.Enums.BoatActionEnum;
+import mock.exceptions.CommandConstructionException;
+import mock.model.MockRace;
+import mock.model.commandFactory.Command;
+import mock.model.commandFactory.CommandFactory;
+import mock.model.commandFactory.CompositeCommand;
+import network.Messages.AC35Data;
+import network.Messages.BoatAction;
+import network.Messages.Enums.MessageType;
+import shared.model.RunnableWithFramePeriod;
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.net.Socket;
-import java.util.Observable;
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
/**
* Service for dispatching key press data to race from client
*/
-public class ControllerServer extends Observable implements Runnable {
+public class ControllerServer implements RunnableWithFramePeriod {
+
+
/**
- * Socket to client
+ * Queue of incoming messages from client.
*/
- private Socket socket;
+ private BlockingQueue inputQueue;
+
+
/**
- * Wrapper for input from client
+ * Collection of commands from client for race to execute.
*/
- private DataInputStream inputStream;
+ private CompositeCommand compositeCommand;
+
/**
- * Last received boat action
+ * The context for each command.
*/
- private BoatActionEnum action;
+ private MockRace raceState;
+
/**
- *
+ * This is the source ID associated with the client.
*/
- private RaceLogic rc;
+ private int clientSourceID;
+
+
/**
- * Initialise server-side controller with live client socket
- * @param socket to client
+ * Initialise server-side controller with live client socket.
+ * @param compositeCommand Commands for the race to execute.
+ * @param inputQueue The queue of messages to read from.
+ * @param clientSourceID The source ID of the client's boat.
+ * @param raceState The context for each command.
*/
- public ControllerServer(Socket socket, RaceLogic rc) {
- this.socket = socket;
- this.rc = rc;
- this.addObserver(rc);
- try {
- this.inputStream = new DataInputStream(this.socket.getInputStream());
- } catch (IOException e) {
- e.printStackTrace();
- }
+ public ControllerServer(CompositeCommand compositeCommand, BlockingQueue inputQueue, int clientSourceID, MockRace raceState) {
+ this.compositeCommand = compositeCommand;
+ this.inputQueue = inputQueue;
+ this.clientSourceID = clientSourceID;
+ this.raceState = raceState;
}
- public BoatActionEnum getAction() {
- return action;
- }
+
+
/**
* Wait for controller key input from client and loop.
*/
@Override
public void run() {
- while(true) {
- byte[] message = new byte[20];
+ while(!Thread.interrupted()) {
+
try {
- if (inputStream.available() > 0) {
- inputStream.read(message);
- BinaryMessageDecoder encodedMessage = new BinaryMessageDecoder(message);
- BoatActionDecoder boatActionDecoder = new BoatActionDecoder(encodedMessage.getMessageBody());
- action = boatActionDecoder.getBoatAction();
-
- // Notify observers of most recent action
- this.notifyObservers();
- this.setChanged();
+
+ AC35Data message = inputQueue.take();
+
+ if (message.getType() == MessageType.BOATACTION) {
+
+ BoatAction boatAction = (BoatAction) message;
+
+
+ boatAction.setSourceID(clientSourceID);
+
+ try {
+ Command command = CommandFactory.createCommand(raceState, boatAction);
+ compositeCommand.addCommand(command);
+
+ } catch (CommandConstructionException e) {
+ Logger.getGlobal().log(Level.WARNING, "ControllerServer could not create a Command for BoatAction: " + boatAction + ".", e);
+
+ }
}
- } catch (IOException e) {
- e.printStackTrace();
+
+
+ } catch (InterruptedException e) {
+ Logger.getGlobal().log(Level.WARNING, "ControllerServer Interrupted while waiting for message on incoming message queue.", e);
+ Thread.currentThread().interrupt();
+ return;
+
}
+
}
+
}
}
diff --git a/racevisionGame/src/main/java/visualiser/gameController/InputChecker.java b/racevisionGame/src/main/java/visualiser/gameController/InputChecker.java
index 34a7d544..057b5721 100644
--- a/racevisionGame/src/main/java/visualiser/gameController/InputChecker.java
+++ b/racevisionGame/src/main/java/visualiser/gameController/InputChecker.java
@@ -7,8 +7,6 @@ import visualiser.gameController.Keys.KeyFactory;
import java.util.HashMap;
-import static javafx.application.Application.launch;
-
/**
* Class for checking what keys are currently being used
*/
@@ -28,7 +26,7 @@ public class InputChecker {
ControlKey controlKey = keyFactory.getKey(codeString);
if (controlKey != null) {
controlKey.onAction();
- System.out.println(controlKey.toString() + " is Pressed.");
+// System.out.println(controlKey.toString() + " is Pressed.");
}
currentlyActiveKeys.put(codeString, true);
}
@@ -39,7 +37,7 @@ public class InputChecker {
ControlKey controlKey = keyFactory.getKey(codeString);
if (controlKey != null) {
controlKey.onRelease();
- System.out.println(controlKey.toString() + " is Released.");
+// System.out.println(controlKey.toString() + " is Released.");
}
currentlyActiveKeys.remove(event.getCode().toString());
});
@@ -51,7 +49,7 @@ public class InputChecker {
ControlKey controlKey = keyFactory.getKey(key);
if (controlKey != null){
controlKey.onHold();
- System.out.println(controlKey.toString() + " is Held.");
+// System.out.println(controlKey.toString() + " is Held.");
}
}
// for (String key : InputKeys.stringKeysMap.keySet()){
diff --git a/racevisionGame/src/main/java/visualiser/model/Annotations.java b/racevisionGame/src/main/java/visualiser/model/Annotations.java
index 09e3831a..54976c35 100644
--- a/racevisionGame/src/main/java/visualiser/model/Annotations.java
+++ b/racevisionGame/src/main/java/visualiser/model/Annotations.java
@@ -40,6 +40,7 @@ public class Annotations {
private static String pathCheckAnno = "showBoatPath";
private static String timeCheckAnno = "showTime";
private static String estTimeCheckAnno = "showEstTime";
+ private static String guideLineAnno = "showGuideline";
// string values match the fx:id value of radio buttons
private static String noBtn = "noBtn";
@@ -160,6 +161,16 @@ public class Annotations {
raceMap.draw();
}
});
+
+ //listener to show estimated time for annotation
+ checkBoxes.get(guideLineAnno).selectedProperty()
+ .addListener((ov, old_val, new_val) -> {
+ if (old_val != new_val) {
+ raceMap.toggleGuideLine();
+ storeCurrentAnnotationState(guideLineAnno, new_val);
+ raceMap.draw();
+ }
+ });
}
/**
diff --git a/racevisionGame/src/main/java/visualiser/model/Ping.java b/racevisionGame/src/main/java/visualiser/model/Ping.java
index b81a3e0c..b31716b5 100644
--- a/racevisionGame/src/main/java/visualiser/model/Ping.java
+++ b/racevisionGame/src/main/java/visualiser/model/Ping.java
@@ -22,16 +22,14 @@ public class Ping implements Runnable {
this.rc = rc;
}
- public boolean pingPort(){
- //TODO the connection needs to be moved to its own thread, so it doesn't block fx thread.
+ public boolean pingPort() {
InetSocketAddress i = new InetSocketAddress(hostname, port);
try (Socket s = new Socket()){
- s.connect(i, 750);//TODO this should be at least a second or two, once moved to its own thread
+ s.connect(i, 1500);
s.shutdownInput();
s.shutdownOutput();
s.close();
rc.statusProperty().set("Ready");
- //System.out.println(String.valueOf(s.isClosed()));
return true;
} catch (IOException e) {
rc.statusProperty().set("Offline");
diff --git a/racevisionGame/src/main/java/visualiser/model/RaceMap.java b/racevisionGame/src/main/java/visualiser/model/RaceMap.java
index 1583e0a5..96e60347 100644
--- a/racevisionGame/src/main/java/visualiser/model/RaceMap.java
+++ b/racevisionGame/src/main/java/visualiser/model/RaceMap.java
@@ -11,22 +11,22 @@ public class RaceMap {
/**
* The longitude of the left side of the map.
*/
- private final double longLeft;
+ private double longLeft;
/**
* The longitude of the right side of the map.
*/
- private final double longRight;
+ private double longRight;
/**
* The latitude of the top side of the map.
*/
- private final double latTop;
+ private double latTop;
/**
* The latitude of the bottom side of the map.
*/
- private final double latBottom;
+ private double latBottom;
/**
@@ -143,4 +143,27 @@ public class RaceMap {
public void setHeight(int height) {
this.height = height;
}
+
+
+ /**
+ * Updates the bottom right GPS coordinates of the RaceMap.
+ * @param bottomRight New bottom right GPS coordinates.
+ */
+ public void setGPSBotRight(GPSCoordinate bottomRight) {
+ this.latBottom = bottomRight.getLatitude();
+ this.longRight = bottomRight.getLongitude();
+ }
+
+
+ /**
+ * Updates the top left GPS coordinates of the RaceMap.
+ * @param topLeft New top left GPS coordinates.
+ */
+ public void setGPSTopLeft(GPSCoordinate topLeft) {
+ this.latTop = topLeft.getLatitude();
+ this.longLeft = topLeft.getLongitude();
+ }
+
+
+
}
diff --git a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java
index 7664f854..d58e2afd 100644
--- a/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java
+++ b/racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java
@@ -1,18 +1,15 @@
package visualiser.model;
-import javafx.scene.Node;
-import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.transform.Rotate;
import network.Messages.Enums.BoatStatusEnum;
import shared.dataInput.RaceDataSource;
-import shared.model.GPSCoordinate;
-import shared.model.Mark;
-import shared.model.RaceClock;
+import shared.enums.RoundingType;
+import shared.model.*;
-import java.time.Duration;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -37,13 +34,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
/**
* The race we read data from and draw.
*/
- private VisualiserRace visualiserRace;
-
- /**
- * The background of the race.
- * We render the background whenever the race boundary changes, or the screen size changes.
- */
- private Image background;
+ private VisualiserRaceEvent visualiserRace;
private boolean annoName = true;
@@ -52,26 +43,20 @@ public class ResizableRaceCanvas extends ResizableCanvas {
private boolean annoPath = true;
private boolean annoEstTime = true;
private boolean annoTimeSinceLastMark = true;
+ private boolean annoGuideLine = false;
- /**
- * The wind arrow node.
- */
- private Node arrow;
-
/**
- * Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRace}.
+ * Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRaceEvent}.
* @param visualiserRace The race that data is read from in order to be drawn.
- * @param arrow The wind arrow's node.
*/
- public ResizableRaceCanvas(VisualiserRace visualiserRace, Node arrow) {
+ public ResizableRaceCanvas(VisualiserRaceEvent visualiserRace) {
super();
this.visualiserRace = visualiserRace;
- this.arrow = arrow;
- RaceDataSource raceData = visualiserRace.getRaceDataSource();
+ RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource();
double lat1 = raceData.getMapTopLeft().getLatitude();
double long1 = raceData.getMapTopLeft().getLongitude();
@@ -126,6 +111,13 @@ public class ResizableRaceCanvas extends ResizableCanvas {
annoSpeed = !annoSpeed;
}
+ /**
+ * Toggle the guideline annotation
+ */
+ public void toggleGuideLine() {
+ annoGuideLine = !annoGuideLine;
+ }
+
@@ -278,8 +270,8 @@ public class ResizableRaceCanvas extends ResizableCanvas {
boat.getCountry(),
boat.getCurrentSpeed(),
this.map.convertGPS(boat.getCurrentPosition()),
- boat.getTimeToNextMarkFormatted(this.visualiserRace.getRaceClock().getCurrentTime()),
- boat.getTimeSinceLastMarkFormatted(this.visualiserRace.getRaceClock().getCurrentTime()) );
+ boat.getTimeToNextMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()),
+ boat.getTimeSinceLastMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()) );
}
@@ -291,7 +283,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
*/
private void drawBoats() {
- for (VisualiserBoat boat : visualiserRace.getBoats()) {
+ for (VisualiserBoat boat : new ArrayList<>(visualiserRace.getVisualiserRaceState().getBoats())) {
//Draw the boat.
drawBoat(boat);
@@ -304,7 +296,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
//If the race hasn't started, we set the time since last mark to the current time, to ensure we don't start counting until the race actually starts.
if ((boat.getStatus() != BoatStatusEnum.RACING) && (boat.getStatus() == BoatStatusEnum.FINISHED)) {
- boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime());
+ boat.setTimeAtLastMark(visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime());
}
//Draw boat label.
@@ -325,35 +317,68 @@ public class ResizableRaceCanvas extends ResizableCanvas {
*/
private void drawBoat(VisualiserBoat boat) {
- //The position may be null if we haven't received any BoatLocation messages yet.
- if (boat.getCurrentPosition() != null) {
+ if (boat.isClientBoat()) {
+ drawClientBoat(boat);
+ }
- //Convert position to graph coordinate.
- GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
+ //Convert position to graph coordinate.
+ GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
- //The x coordinates of each vertex of the boat.
- double[] x = {
- pos.getX() - 6,
- pos.getX(),
- pos.getX() + 6 };
+ //The x coordinates of each vertex of the boat.
+ double[] x = {
+ pos.getX() - 6,
+ pos.getX(),
+ pos.getX() + 6 };
- //The y coordinates of each vertex of the boat.
- double[] y = {
- pos.getY() + 12,
- pos.getY() - 12,
- pos.getY() + 12 };
+ //The y coordinates of each vertex of the boat.
+ double[] y = {
+ pos.getY() + 12,
+ pos.getY() - 12,
+ pos.getY() + 12 };
- //The above shape is essentially a triangle 12px wide, and 24px long.
+ //The above shape is essentially a triangle 12px wide, and 24px long.
- //Draw the boat.
- gc.setFill(boat.getColor());
+ //Draw the boat.
+ gc.setFill(boat.getColor());
- gc.save();
- rotate(boat.getBearing().degrees(), pos.getX(), pos.getY());
- gc.fillPolygon(x, y, 3);
- gc.restore();
+ gc.save();
+ rotate(boat.getBearing().degrees(), pos.getX(), pos.getY());
+ gc.fillPolygon(x, y, 3);
+ gc.restore();
- }
+
+ }
+
+ /**
+ * Draws extra decorations to show which boat has been assigned to the client.
+ * @param boat The client's boat.
+ */
+ private void drawClientBoat(VisualiserBoat boat) {
+
+ //Convert position to graph coordinate.
+ GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
+
+ //The x coordinates of each vertex of the boat.
+ double[] x = {
+ pos.getX() - 9,
+ pos.getX(),
+ pos.getX() + 9 };
+
+ //The y coordinates of each vertex of the boat.
+ double[] y = {
+ pos.getY() + 15,
+ pos.getY() - 15,
+ pos.getY() + 15 };
+
+ //The above shape is essentially a triangle 24px wide, and 48 long.
+
+ //Draw the boat.
+ gc.setFill(Color.BLACK);
+
+ gc.save();
+ rotate(boat.getBearing().degrees(), pos.getX(), pos.getY());
+ gc.fillPolygon(x, y, 3);
+ gc.restore();
}
@@ -375,37 +400,12 @@ public class ResizableRaceCanvas extends ResizableCanvas {
- /**
- * Displays an arrow representing wind direction on the Canvas.
- * This function accepts a wind-to bearing, but displays a wind-from bearing.
- *
- * @param angle Angle that the arrow is to be facing in degrees 0 degrees = North (Up).
- * @see GraphCoordinate
- */
- private void displayWindArrow(double angle) {
-
- //We need to display wind-from, so add 180 degrees.
- angle += 180d;
-
- //Get it within [0, 360).
- while (angle >= 360d) {
- angle -= 360d;
- }
-
- //Rotate the wind arrow.
- if (arrow != null && arrow.getRotate() != angle) {
- arrow.setRotate(angle);
- }
- }
-
-
-
-
/**
* Draws all of the {@link Mark}s on the canvas.
*/
private void drawMarks() {
- for (Mark mark : this.visualiserRace.getMarks()) {
+
+ for (Mark mark : new ArrayList<>(visualiserRace.getVisualiserRaceState().getMarks())) {
drawMark(mark);
}
}
@@ -440,13 +440,9 @@ public class ResizableRaceCanvas extends ResizableCanvas {
this.map.setWidth((int) getWidth());
this.map.setHeight((int) getHeight());
- //Redraw the boundary.
- redrawBoundaryImage();
-
//Draw the race.
drawRace();
-
}
@@ -459,19 +455,17 @@ public class ResizableRaceCanvas extends ResizableCanvas {
/**
- * Draws the race boundary, and saves the image to {@link #background}.
- * You should call {@link #clear()} before calling this.
+ * Draws the race boundary.
*/
- private void redrawBoundaryImage() {
+ private void drawBoundary() {
//Prepare to draw.
gc.setLineWidth(1);
gc.setFill(Color.AQUA);
- gc.drawImage(new Image(getClass().getClassLoader().getResourceAsStream("images/WaterBackground.png")), 0, 0);
//Calculate the screen coordinates of the boundary.
- List boundary = this.visualiserRace.getBoundary();
+ List boundary = new ArrayList<>(visualiserRace.getVisualiserRaceState().getBoundary());
double[] xpoints = new double[boundary.size()];
double[] ypoints = new double[boundary.size()];
@@ -487,9 +481,6 @@ public class ResizableRaceCanvas extends ResizableCanvas {
//Draw the boundary.
gc.fillPolygon(xpoints, ypoints, xpoints.length);
- //Render boundary to image.
- this.background = snapshot(null, null);
-
}
/**
@@ -498,6 +489,10 @@ public class ResizableRaceCanvas extends ResizableCanvas {
*/
public void drawRace() {
+ //Update RaceMap with new GPS values of race.
+ this.map.setGPSTopLeft(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapTopLeft());
+ this.map.setGPSBotRight(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapBottomRight());
+
gc.setLineWidth(2);
clear();
@@ -505,26 +500,153 @@ public class ResizableRaceCanvas extends ResizableCanvas {
//Race boundary.
drawBoundary();
+ //Guiding Line
+ if (annoGuideLine){
+ drawRaceLine();
+ }
+
//Boats.
drawBoats();
//Marks.
drawMarks();
- //Wind arrow. This rotates the wind arrow node.
- displayWindArrow(this.visualiserRace.getWindDirection().degrees());
-
}
+ /**
+ * draws a transparent line around the course that shows the paths boats must travel
+ */
+ public void drawRaceLine(){
+ List legs = this.visualiserRace.getVisualiserRaceState().getLegs();
+ GPSCoordinate legStartPoint = legs.get(0).getStartCompoundMark().getAverageGPSCoordinate();
+ GPSCoordinate nextStartPoint;
+ for (int i = 0; i < legs.size() -1; i++) {
+ nextStartPoint = drawLineRounding(legs, i, legStartPoint);
+ legStartPoint = nextStartPoint;
+ }
+ }
/**
- * Draws the race boundary image onto the canvas.
- * See {@link #background}.
+ * Draws a line around a course that shows where boats need to go. This method
+ * draws the line leg by leg
+ * @param legs the legs of a race
+ * @param index the index of the current leg to use
+ * @param legStartPoint The position the current leg.
+ * @return the end point of the current leg that has been drawn
*/
- private void drawBoundary() {
- gc.drawImage(this.background, 0, 0);
+ private GPSCoordinate drawLineRounding(List legs, int index, GPSCoordinate legStartPoint){
+ GPSCoordinate startDirectionLinePoint;
+ GPSCoordinate endDirectionLinePoint;
+ Bearing bearingOfDirectionLine;
+
+ GPSCoordinate startNextDirectionLinePoint;
+ GPSCoordinate endNextDirectionLinePoint;
+ Bearing bearingOfNextDirectionLine;
+
+ //finds the direction of the current leg as a bearing
+ startDirectionLinePoint = legStartPoint;
+ GPSCoordinate tempEndDirectionLinePoint = legs.get(index).getEndCompoundMark().getAverageGPSCoordinate();
+
+ bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, tempEndDirectionLinePoint);
+
+ //after finding the initial bearing pick the mark used for rounding
+ endDirectionLinePoint = legs.get(index).getEndCompoundMark().getMarkForRounding(bearingOfDirectionLine).getPosition();
+ bearingOfDirectionLine = GPSCoordinate.calculateBearing(startDirectionLinePoint, endDirectionLinePoint);
+
+ //finds the direction of the next leg as a bearing
+ if (index < legs.size() -2){ // not last leg
+ startNextDirectionLinePoint = legs.get(index + 1).getStartCompoundMark().getMark1Position();
+ endNextDirectionLinePoint = legs.get(index + 1).getEndCompoundMark().getMark1Position();
+ bearingOfNextDirectionLine = GPSCoordinate.calculateBearing(startNextDirectionLinePoint, endNextDirectionLinePoint);
+
+ double degreesToAdd;
+ //find which side is need to be used
+ if (legs.get(index).getEndCompoundMark().getRoundingType() == RoundingType.Port ||
+ legs.get(index).getEndCompoundMark().getRoundingType() == RoundingType.SP){
+ degreesToAdd = 90;
+ }else{
+ degreesToAdd = -90;
+ }
+
+ //use the direction line to find a point parallel to it by the mark
+ GPSCoordinate pointToStartCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint,
+ 100, Azimuth.fromDegrees(bearingOfDirectionLine.degrees()+degreesToAdd));
+
+ //use the direction line to find a point to curve too
+ GPSCoordinate pointToEndCurve = GPSCoordinate.calculateNewPosition(endDirectionLinePoint,
+ 100, Azimuth.fromDegrees(bearingOfNextDirectionLine.degrees()+degreesToAdd));
+
+ //use the curve points to find the two control points for the bezier curve
+ GPSCoordinate controlPoint;
+ GPSCoordinate controlPoint2;
+ Bearing bearingOfCurveLine = GPSCoordinate.calculateBearing(pointToStartCurve, pointToEndCurve);
+ if ((bearingOfDirectionLine.degrees() - bearingOfNextDirectionLine.degrees() +360)%360< 145){
+ //small turn
+ controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve,
+ 50, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+(degreesToAdd/2)));
+ controlPoint2 = controlPoint;
+ }else{
+ //large turn
+ controlPoint = GPSCoordinate.calculateNewPosition(pointToStartCurve,
+ 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+degreesToAdd));
+ controlPoint2 = GPSCoordinate.calculateNewPosition(pointToEndCurve,
+ 150, Azimuth.fromDegrees(bearingOfCurveLine.degrees()+degreesToAdd));
+ }
+
+
+ //change all gps into graph coordinate
+ GraphCoordinate startPath = this.map.convertGPS(startDirectionLinePoint);
+ GraphCoordinate curvePoint = this.map.convertGPS(pointToStartCurve);
+ GraphCoordinate curvePointEnd = this.map.convertGPS(pointToEndCurve);
+ GraphCoordinate c1 = this.map.convertGPS(controlPoint);
+ GraphCoordinate c2 = this.map.convertGPS(controlPoint2);
+
+ gc.setLineWidth(2);
+ gc.setStroke(Color.MEDIUMAQUAMARINE);
+
+ gc.beginPath();
+ gc.moveTo(startPath.getX(), startPath.getY());
+ gc.lineTo(curvePoint.getX(), curvePoint.getY());
+ drawArrowHead(startDirectionLinePoint, pointToStartCurve);
+ gc.bezierCurveTo(c1.getX(), c1.getY(), c2.getX(), c2.getY(), curvePointEnd.getX(), curvePointEnd.getY());
+ gc.stroke();
+ gc.closePath();
+ gc.save();
+
+ return pointToEndCurve;
+ }else{//last leg so no curve
+ GraphCoordinate startPath = this.map.convertGPS(legStartPoint);
+ GraphCoordinate endPath = this.map.convertGPS(legs.get(index).getEndCompoundMark().getAverageGPSCoordinate());
+
+ gc.beginPath();
+ gc.moveTo(startPath.getX(), startPath.getY());
+ gc.lineTo(endPath.getX(), endPath.getY());
+ gc.stroke();
+ gc.closePath();
+ gc.save();
+ drawArrowHead(legStartPoint, legs.get(index).getEndCompoundMark().getAverageGPSCoordinate());
+ return null;
+ }
}
+ private void drawArrowHead(GPSCoordinate start, GPSCoordinate end){
+
+ GraphCoordinate lineStart = this.map.convertGPS(start);
+ GraphCoordinate lineEnd = this.map.convertGPS(end);
+
+ double arrowAngle = Math.toRadians(45.0);
+ double arrowLength = 10.0;
+ double dx = lineStart.getX() - lineEnd.getX();
+ double dy = lineStart.getY() - lineEnd.getY();
+ double angle = Math.atan2(dy, dx);
+ double x1 = Math.cos(angle + arrowAngle) * arrowLength + lineEnd.getX();
+ double y1 = Math.sin(angle + arrowAngle) * arrowLength + lineEnd.getY();
+
+ double x2 = Math.cos(angle - arrowAngle) * arrowLength + lineEnd.getX();
+ double y2 = Math.sin(angle - arrowAngle) * arrowLength + lineEnd.getY();
+ gc.strokeLine(lineEnd.getX(), lineEnd.getY(), x1, y1);
+ gc.strokeLine(lineEnd.getX(), lineEnd.getY(), x2, y2);
+ }
@@ -536,7 +658,6 @@ public class ResizableRaceCanvas extends ResizableCanvas {
* @see TrackPoint
*/
private void drawTrack(VisualiserBoat boat) {
-
//Check that track points are enabled.
if (this.annoPath) {
@@ -544,7 +665,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
gc.setFill(boat.getColor());
//Draw each TrackPoint.
- for (TrackPoint point : boat.getTrack()) {
+ for (TrackPoint point : new ArrayList<>(boat.getTrack())) {
//Convert the GPSCoordinate to a screen coordinate.
GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate());
diff --git a/racevisionGame/src/main/java/visualiser/model/Sparkline.java b/racevisionGame/src/main/java/visualiser/model/Sparkline.java
index 5e802ff9..3f7e07b8 100644
--- a/racevisionGame/src/main/java/visualiser/model/Sparkline.java
+++ b/racevisionGame/src/main/java/visualiser/model/Sparkline.java
@@ -1,13 +1,18 @@
package visualiser.model;
import javafx.application.Platform;
+import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
+import javafx.collections.transformation.SortedList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.paint.Color;
+import shared.model.Leg;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
@@ -23,7 +28,7 @@ public class Sparkline {
/**
* The race to observe.
*/
- private VisualiserRace race;
+ private VisualiserRaceState race;
/**
* The boats to observe.
@@ -31,10 +36,10 @@ public class Sparkline {
private ObservableList boats;
/**
- * The number of legs in the race.
- * Used to correctly scale the linechart.
+ * Race legs to observe.
+ * We need to observe legs as they may be added after the sparkline is created if race.xml is received after this is created.
*/
- private Integer legNum;
+ private ObservableList legs;
/**
@@ -52,21 +57,31 @@ public class Sparkline {
*/
private NumberAxis yAxis;
+ /**
+ * A map between a boat and its data series in the sparkline.
+ * This is used so that we can remove a series when (or if) a boat is removed from the race.
+ */
+ private Map> boatSeriesMap;
+
+
+
/**
* Constructor to set up initial sparkline (LineChart) object
* @param race The race to listen to.
* @param sparklineChart JavaFX LineChart for the sparkline.
*/
- public Sparkline(VisualiserRace race, LineChart sparklineChart) {
+ public Sparkline(VisualiserRaceState race, LineChart sparklineChart) {
this.race = race;
- this.boats = race.getBoats();
- this.legNum = race.getLegCount();
+ this.boats = new SortedList<>(race.getBoats());
+ this.legs = race.getLegs();
this.sparklineChart = sparklineChart;
this.yAxis = (NumberAxis) sparklineChart.getYAxis();
this.xAxis = (NumberAxis) sparklineChart.getXAxis();
+ this.boatSeriesMap = new HashMap<>();
+
createSparkline();
}
@@ -78,50 +93,45 @@ public class Sparkline {
* Position numbers are displayed.
*/
private void createSparkline() {
- // NOTE: Y axis is in negatives to display correct positions
-
- //For each boat...
- for (VisualiserBoat boat : this.boats) {
- //Create data series for each boat.
- XYChart.Series series = new XYChart.Series<>();
+ //We need to dynamically update the sparkline when boats are added/removed.
+ boats.addListener((ListChangeListener.Change extends VisualiserBoat> c) -> {
+ Platform.runLater(() -> {
- //All boats start in "last" place.
- series.getData().add(new XYChart.Data<>(0, boats.size()));
+ while (c.next()) {
- //Listen for changes in the boat's leg - we only update the graph when it changes leg.
- boat.legProperty().addListener(
- (observable, oldValue, newValue) -> {
+ if (c.wasAdded()) {
+ for (VisualiserBoat boat : c.getAddedSubList()) {
+ addBoatSeries(boat);
+ }
- //Get the data to plot.
- List boatOrder = race.getLegCompletionOrder().get(oldValue);
- //Find boat position in list.
- int boatPosition = boatOrder.indexOf(boat) + 1;
+ } else if (c.wasRemoved()) {
+ for (VisualiserBoat boat : c.getRemoved()) {
+ removeBoatSeries(boat);
+ }
+ }
- //Get leg number.
- int legNumber = oldValue.getLegNumber() + 1;
+ }
+ //Update height of y axis.
+ setYAxisLowerBound();
+ });
- //Create new data point for boat's position at the new leg.
- XYChart.Data dataPoint = new XYChart.Data<>(legNumber, boatPosition);
+ });
- //Add to series.
- Platform.runLater(() -> series.getData().add(dataPoint));
-
- });
-
-
- //Add to chart.
- sparklineChart.getData().add(series);
-
- //Color using boat's color. We need to do this after adding the series to a chart, otherwise we get null pointer exceptions.
- series.getNode().setStyle("-fx-stroke: " + colourToHex(boat.getColor()) + ";");
+ legs.addListener((ListChangeListener.Change extends Leg> c) -> {
+ Platform.runLater(() -> xAxis.setUpperBound(race.getLegCount()));
+ });
+ //Initialise chart for existing boats.
+ for (VisualiserBoat boat : boats) {
+ addBoatSeries(boat);
}
+
sparklineChart.setCreateSymbols(false);
//Set x axis details
@@ -130,20 +140,109 @@ public class Sparkline {
xAxis.setTickLabelsVisible(false);
xAxis.setMinorTickVisible(false);
xAxis.setLowerBound(0);
- xAxis.setUpperBound(legNum + 2);
+ xAxis.setUpperBound(race.getLegCount());
xAxis.setTickUnit(1);
+
+ //The y-axis uses negative values, with the minus sign hidden (e.g., boat in 1st has position -1, which becomes 1, boat in 6th has position -6, which becomes 6).
+ //This is necessary to actually get the y-axis labelled correctly. Negative tick count doesn't work.
//Set y axis details
- yAxis.setLowerBound(boats.size());
- yAxis.setUpperBound(1);
yAxis.setAutoRanging(false);
+
+ yAxis.setTickUnit(1);
+ yAxis.setMinorTickCount(0);
+
+ yAxis.setUpperBound(0);
+ setYAxisLowerBound();
+
yAxis.setLabel("Position in Race");
- yAxis.setTickUnit(-1);//Negative tick reverses the y axis.
yAxis.setTickMarkVisible(true);
yAxis.setTickLabelsVisible(true);
- yAxis.setTickMarkVisible(true);
- yAxis.setMinorTickVisible(true);
+ yAxis.setMinorTickVisible(false);
+
+
+
+ //Hide minus number from displaying on axis.
+ yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis) {
+ @Override
+ public String toString(Number value) {
+ if ((value.intValue() == 0) || (value.intValue() < -boats.size())) {
+ return "";
+
+ } else {
+ return String.format("%d", -value.intValue());
+ }
+ }
+ });
+
+ }
+
+
+ /**
+ * Sets the lower bound of the y-axis.
+ */
+ private void setYAxisLowerBound() {
+ yAxis.setLowerBound( -(boats.size() + 1));
+ }
+
+
+
+ /**
+ * Removes the data series for a given boat from the sparkline.
+ * @param boat Boat to remove series for.
+ */
+ private void removeBoatSeries(VisualiserBoat boat) {
+ sparklineChart.getData().remove(boatSeriesMap.get(boat));
+ boatSeriesMap.remove(boat);
+ }
+
+
+ /**
+ * Creates a data series for a boat, and adds it to the sparkline.
+ * @param boat Boat to add series for.
+ */
+ private void addBoatSeries(VisualiserBoat boat) {
+
+ //Create data series for boat.
+ XYChart.Series series = new XYChart.Series<>();
+
+
+ //All boats start in "last" place.
+ series.getData().add(new XYChart.Data<>(0, -(boats.size())));
+
+ //Listen for changes in the boat's leg - we only update the graph when it changes leg.
+ boat.legProperty().addListener(
+ (observable, oldValue, newValue) -> {
+
+ //Get the data to plot.
+ List boatOrder = race.getLegCompletionOrder().get(oldValue);
+ //Find boat position in list.
+ int boatPosition = -(boatOrder.indexOf(boat) + 1);
+
+ //Get leg number.
+ int legNumber = oldValue.getLegNumber() + 1;
+
+
+ //Create new data point for boat's position at the new leg.
+ XYChart.Data dataPoint = new XYChart.Data<>(legNumber, boatPosition);
+
+ //Add to series.
+ Platform.runLater(() -> series.getData().add(dataPoint));
+
+
+ });
+
+
+ //Add to chart.
+ sparklineChart.getData().add(series);
+
+ //Color using boat's color. We need to do this after adding the series to a chart, otherwise we get null pointer exceptions.
+ series.getNode().setStyle("-fx-stroke: " + colourToHex(boat.getColor()) + ";");
+
+
+ boatSeriesMap.put(boat, series);
+
}
diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java
index 57ba0790..ce966b02 100644
--- a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java
+++ b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java
@@ -57,6 +57,11 @@ public class VisualiserBoat extends Boat {
*/
private static final double wakeScale = 5;
+ /**
+ * If true then this boat has been allocated to the client.
+ */
+ private boolean isClientBoat = false;
+
@@ -169,28 +174,33 @@ public class VisualiserBoat extends Boat {
*/
public String getTimeToNextMarkFormatted(ZonedDateTime currentTime) {
- //Calculate time delta.
- Duration timeUntil = Duration.between(currentTime, getEstimatedTimeAtNextMark());
+ if ((getTimeAtLastMark() != null) && (currentTime != null)) {
+ //Calculate time delta.
+ Duration timeUntil = Duration.between(currentTime, getEstimatedTimeAtNextMark());
- //Convert to seconds.
- long secondsUntil = timeUntil.getSeconds();
+ //Convert to seconds.
+ long secondsUntil = timeUntil.getSeconds();
- //This means the estimated time is in the past, or not racing.
- if ((secondsUntil < 0) || (getStatus() != BoatStatusEnum.RACING)) {
- return " -";
- }
+ //This means the estimated time is in the past, or not racing.
+ if ((secondsUntil < 0) || (getStatus() != BoatStatusEnum.RACING)) {
+ return " -";
+ }
- if (secondsUntil <= 60) {
- //If less than 1 minute, display seconds only.
- return " " + secondsUntil + "s";
+ if (secondsUntil <= 60) {
+ //If less than 1 minute, display seconds only.
+ return " " + secondsUntil + "s";
- } else {
- //Otherwise display minutes and seconds.
- long seconds = secondsUntil % 60;
- long minutes = (secondsUntil - seconds) / 60;
- return String.format(" %dm %ds", minutes, seconds);
+ } else {
+ //Otherwise display minutes and seconds.
+ long seconds = secondsUntil % 60;
+ long minutes = (secondsUntil - seconds) / 60;
+ return String.format(" %dm %ds", minutes, seconds);
+
+ }
+ } else {
+ return " -";
}
}
@@ -203,7 +213,7 @@ public class VisualiserBoat extends Boat {
*/
public String getTimeSinceLastMarkFormatted(ZonedDateTime currentTime) {
- if (getTimeAtLastMark() != null) {
+ if ((getTimeAtLastMark() != null) && (currentTime != null)) {
//Calculate time delta.
Duration timeSince = Duration.between(getTimeAtLastMark(), currentTime);
@@ -214,4 +224,21 @@ public class VisualiserBoat extends Boat {
return " -";
}
}
+
+
+ /**
+ * Returns whether or not this boat has been assigned to the client.
+ * @return True if this is the client's boat.
+ */
+ public boolean isClientBoat() {
+ return isClientBoat;
+ }
+
+ /**
+ * Sets whether or not this boat has been assigned to the client.
+ * @param clientBoat True if this is the client's boat.
+ */
+ public void setClientBoat(boolean clientBoat) {
+ isClientBoat = clientBoat;
+ }
}
diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java
deleted file mode 100644
index 2969fbb8..00000000
--- a/racevisionGame/src/main/java/visualiser/model/VisualiserRace.java
+++ /dev/null
@@ -1,470 +0,0 @@
-package visualiser.model;
-
-import javafx.animation.AnimationTimer;
-import javafx.collections.FXCollections;
-import javafx.collections.ObservableList;
-import javafx.scene.paint.Color;
-import network.Messages.BoatLocation;
-import network.Messages.BoatStatus;
-import network.Messages.Enums.BoatStatusEnum;
-import network.Messages.Enums.RaceStatusEnum;
-import network.Messages.LatestMessages;
-import network.Messages.RaceStatus;
-import shared.dataInput.BoatDataSource;
-import shared.dataInput.RaceDataSource;
-import shared.dataInput.RegattaDataSource;
-import shared.model.*;
-
-import java.time.Duration;
-import java.time.ZonedDateTime;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * The Class used to view the race streamed.
- * Has a course, boats, boundaries, etc...
- * Observes LatestMessages and updates its state based on new messages.
- */
-public class VisualiserRace extends Race implements Runnable {
-
-
- /**
- * An observable list of boats in the race.
- */
- private final ObservableList boats;
-
- /**
- * An observable list of marker boats in the race.
- */
- private ObservableList boatMarkers;
-
-
- /**
- * Maps between a Leg to a list of boats, in the order that they finished the leg.
- * Used by the Sparkline to ensure it has correct information.
- */
- private Map> legCompletionOrder = new HashMap<>();
-
-
-
- /**
- * Constructs a race object with a given RaceDataSource, BoatDataSource, and RegattaDataSource and receives events from LatestMessages.
- * @param boatDataSource Data source for boat related data (yachts and marker boats).
- * @param raceDataSource Data source for race related data (participating boats, legs, etc...).
- * @param regattaDataSource Data source for race related data (course name, location, timezone, etc...).
- * @param latestMessages The LatestMessages to send events to.
- * @param colors A collection of colors used to assign a color to each boat.
- */
- public VisualiserRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, List colors) {
-
- super(boatDataSource, raceDataSource, regattaDataSource, latestMessages);
-
-
- this.boats = FXCollections.observableArrayList(this.generateVisualiserBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), colors));
-
- this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values());
-
-
- //Initialise the leg completion order map.
- for (Leg leg : this.legs) {
- this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size()));
- }
-
- }
-
-
- /**
- * Sets the race data source for this race to a new RaceDataSource.
- * Uses the boundary and legs specified by the new RaceDataSource.
- * @param raceDataSource The new RaceDataSource to use.
- */
- public void setRaceDataSource(RaceDataSource raceDataSource) {
- this.raceDataSource = raceDataSource;
-
- this.boundary = raceDataSource.getBoundary();
-
- this.useLegsList(raceDataSource.getLegs());
- }
-
- /**
- * Sets the boat data source for this race to a new BoatDataSource.
- * Uses the marker boats specified by the new BoatDataSource.
- * @param boatDataSource The new BoatDataSource to use.
- */
- public void setBoatDataSource(BoatDataSource boatDataSource) {
- this.boatDataSource = boatDataSource;
-
- this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values());
- }
-
- /**
- * Sets the regatta data source for this race to a new RegattaDataSource.
- * @param regattaDataSource The new RegattaDataSource to use.
- */
- public void setRegattaDataSource(RegattaDataSource regattaDataSource) {
- this.regattaDataSource = regattaDataSource;
- }
-
-
- /**
- * Returns a list of {@link Mark} boats.
- * @return List of mark boats.
- */
- public ObservableList getMarks() {
- return boatMarkers;
- }
-
- /**
- * Generates a list of VisualiserBoats given a list of Boats, and a list of participating boats.
- * @param boats The map of Boats describing boats that are potentially in the race. Maps boat sourceID to boat.
- * @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating.
- * @param colors The list of colors to be used for the boats.
- * @return A list of MockBoats that are participating in the race.
- */
- private List generateVisualiserBoats(Map boats, List sourceIDs, List colors) {
-
- List visualiserBoats = new ArrayList<>(sourceIDs.size());
-
- //For each sourceID participating...
- int colorIndex = 0;
- for (int sourceID : sourceIDs) {
-
- //Get the boat associated with the sourceID.
- Boat boat = boats.get(sourceID);
-
- //Get a color for the boat.
- Color color = colors.get(colorIndex);
-
- //Construct a VisualiserBoat using the Boat and Polars.
- VisualiserBoat visualiserBoat = new VisualiserBoat(boat, color);
-
- visualiserBoats.add(visualiserBoat);
-
- //Next color.
- colorIndex++;
-
- }
-
- return visualiserBoats;
-
- }
-
-
- /**
- * Initialise the boats in the race.
- * This sets their current leg.
- */
- @Override
- protected void initialiseBoats() {
-
- Leg startingLeg = legs.get(0);
-
- for (VisualiserBoat boat : boats) {
-
- boat.setCurrentLeg(startingLeg);
- boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
-
- }
-
- }
-
-
- /**
- * Updates all of the racing boats based on messages received.
- * @param boats The list of racing boats.
- * @param boatLocationMap A map between boat sourceIDs and BoatLocation messages.
- * @param boatStatusMap A map between boat sourceIDs and BoatStatus messages.
- */
- private void updateBoats(ObservableList boats, Map boatLocationMap, Map boatStatusMap) {
-
- for (VisualiserBoat boat : boats) {
- BoatLocation boatLocation = boatLocationMap.get(boat.getSourceID());
- BoatStatus boatStatus = boatStatusMap.get(boat.getSourceID());
- updateBoat(boat, boatLocation, boatStatus);
- }
- }
-
-
- /**
- * Updates an individual racing boat based on messages received.
- * @param boat The boat to update.
- * @param boatLocation The BoatLocation message to use.
- * @param boatStatus The BoatStatus message to use.
- */
- private void updateBoat(VisualiserBoat boat, BoatLocation boatLocation, BoatStatus boatStatus) {
-
- if (boatLocation != null && boatStatus != null) {
-
- //Get the new position.
- double latitude = boatLocation.getLatitudeDouble();
- double longitude = boatLocation.getLongitudeDouble();
- GPSCoordinate gpsCoordinate = new GPSCoordinate(latitude, longitude);
-
- boat.setCurrentPosition(gpsCoordinate);
-
- //Bearing.
- boat.setBearing(Bearing.fromDegrees(boatLocation.getHeadingDegrees()));
-
- //Time until next mark.
- boat.setEstimatedTimeAtNextMark(raceClock.getLocalTime(boatStatus.getEstTimeAtNextMark()));
-
- //Speed.
- boat.setCurrentSpeed(boatLocation.getBoatSOG() / Constants.KnotsToMMPerSecond);
-
-
- //Boat status.
- BoatStatusEnum newBoatStatusEnum = BoatStatusEnum.fromByte(boatStatus.getBoatStatus());
-
- //If we are changing from non-racing to racing, we need to initialise boat with their time at last mark.
- if ((boat.getStatus() != BoatStatusEnum.RACING) && (newBoatStatusEnum == BoatStatusEnum.RACING)) {
- boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
- }
-
- boat.setStatus(newBoatStatusEnum);
-
-
- //Leg.
- int legNumber = boatStatus.getLegNumber();
-
- if (legNumber >= 1 && legNumber < legs.size()) {
- if (boat.getCurrentLeg() != legs.get(legNumber)) {
- boatFinishedLeg(boat, legs.get(legNumber));
- }
- }
-
-
- //Attempt to add a track point.
- if (newBoatStatusEnum == BoatStatusEnum.RACING) {
- boat.addTrackPoint(boat.getCurrentPosition(), raceClock.getCurrentTime());
- }
-
- //Set finish time if boat finished.
- if (newBoatStatusEnum == BoatStatusEnum.FINISHED || legNumber == this.legs.size()) {
- boat.setTimeFinished(boatLocation.getTime());
- boat.setStatus(BoatStatusEnum.FINISHED);
-
- }
-
- }
- }
-
-
- /**
- * Updates a boat's leg to a specified leg. Also records the order in which the boat passed the leg.
- * @param boat The boat to update.
- * @param leg The leg to use.
- */
- private void boatFinishedLeg(VisualiserBoat boat, Leg leg) {
-
- //Record order in which boat finished leg.
- this.legCompletionOrder.get(boat.getCurrentLeg()).add(boat);
-
- //Update boat.
- boat.setCurrentLeg(leg);
- boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
-
- }
-
-
- /**
- * Updates all of the marker boats based on messages received.
- * @param boatMarkers The list of marker boats.
- * @param boatLocationMap A map between boat sourceIDs and BoatLocation messages.
- * @param boatStatusMap A map between boat sourceIDs and BoatStatus messages.
- */
- private void updateMarkers(ObservableList boatMarkers, Map boatLocationMap, Map boatStatusMap) {
-
- for (Mark mark : boatMarkers) {
- BoatLocation boatLocation = boatLocationMap.get(mark.getSourceID());
- updateMark(mark, boatLocation);
- }
- }
-
- /**
- * Updates an individual marker boat based on messages received.
- * @param mark The marker boat to be updated.
- * @param boatLocation The message describing the boat's new location.
- */
- private void updateMark(Mark mark, BoatLocation boatLocation) {
-
- if (boatLocation != null) {
-
- //We only update the boat's position.
- double latitude = boatLocation.getLatitudeDouble();
- double longitude = boatLocation.getLongitudeDouble();
- GPSCoordinate gpsCoordinate = new GPSCoordinate(latitude, longitude);
-
- mark.setPosition(gpsCoordinate);
-
- }
- }
-
-
- /**
- * Updates the race status (RaceStatusEnum, wind bearing, wind speed) based on received messages.
- * @param raceStatus The RaceStatus message received.
- */
- private void updateRaceStatus(RaceStatus raceStatus) {
-
- //Race status enum.
- this.raceStatusEnum = RaceStatusEnum.fromByte(raceStatus.getRaceStatus());
-
- //Wind.
- this.setWind(
- Bearing.fromDegrees(raceStatus.getScaledWindDirection()),
- raceStatus.getWindSpeedKnots() );
-
- //Current race time.
- this.raceClock.setUTCTime(raceStatus.getCurrentTime());
-
- }
-
-
-
-
- /**
- * Runnable for the thread.
- */
- public void run() {
- initialiseBoats();
- startRaceStream();
- }
-
-
-
-
- /**
- * Starts the race.
- * This updates the race based on {@link #latestMessages}.
- */
- private void startRaceStream() {
-
- new AnimationTimer() {
-
- long lastFrameTime = System.currentTimeMillis();
-
- @Override
- public void handle(long arg0) {
-
- //Calculate the frame period.
- long currentFrameTime = System.currentTimeMillis();
- long framePeriod = currentFrameTime - lastFrameTime;
-
-
- //Update race status.
- updateRaceStatus(latestMessages.getRaceStatus());
-
-
- //Update racing boats.
- updateBoats(boats, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap());
- //And their positions (e.g., 5th).
- updateBoatPositions(boats);
-
-
- //Update marker boats.
- updateMarkers(boatMarkers, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap());
-
-
-
-
- if (getRaceStatusEnum() == RaceStatusEnum.FINISHED) {
- stop();
- }
-
- lastFrameTime = currentFrameTime;
-
- //Increment fps.
- incrementFps(framePeriod);
-
- }
- }.start();
- }
-
- /**
- * Update position of boats in race (e.g, 5th), no position if on starting leg or DNF.
- * @param boats The list of boats to update.
- */
- private void updateBoatPositions(ObservableList boats) {
-
- //Sort boats.
- sortBoatsByPosition(boats);
-
- //Assign new positions.
- for (int i = 0; i < boats.size(); i++) {
- VisualiserBoat boat = boats.get(i);
-
-
- if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) {
-
- boat.setPosition("-");
-
- } else {
- boat.setPosition(Integer.toString(i + 1));
- }
- }
-
- }
-
- /**
- * Sorts the list of boats by their position within the race.
- * @param boats The list of boats in the race.
- */
- private void sortBoatsByPosition(ObservableList boats) {
-
- FXCollections.sort(boats, (a, b) -> {
- //Get the difference in leg numbers.
- int legNumberDelta = b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber();
-
- //If they're on the same leg, we need to compare time to finish leg.
- if (legNumberDelta == 0) {
- return (int) Duration.between(b.getEstimatedTimeAtNextMark(), a.getEstimatedTimeAtNextMark()).toMillis();
- } else {
- return legNumberDelta;
- }
-
- });
-
- }
-
-
-
-
- /**
- * Returns the boats participating in the race.
- * @return ObservableList of boats participating in the race.
- */
- public ObservableList getBoats() {
- return boats;
- }
-
- /**
- * Returns the order in which boats completed each leg. Maps the leg to a list of boats, ordered by the order in which they finished the leg.
- * @return Leg completion order for each leg.
- */
- public Map> getLegCompletionOrder() {
- return legCompletionOrder;
- }
-
- /**
- * Takes an estimated time an event will occur, and converts it to the
- * number of seconds before the event will occur.
- *
- * @param estTimeMillis The estimated time, in milliseconds.
- * @param currentTime The current time, in milliseconds.
- * @return int difference between time the race started and the estimated time
- */
- private int convertEstTime(long estTimeMillis, long currentTime) {
-
- //Calculate millisecond delta.
- long estElapsedMillis = estTimeMillis - currentTime;
-
- //Convert milliseconds to seconds.
- int estElapsedSecs = Math.round(estElapsedMillis / 1000);
-
- return estElapsedSecs;
-
- }
-
-}
diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java
new file mode 100644
index 00000000..748808b3
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java
@@ -0,0 +1,79 @@
+package visualiser.model;
+
+import mock.exceptions.CommandConstructionException;
+import mock.model.commandFactory.Command;
+import mock.model.commandFactory.CompositeCommand;
+import network.Messages.*;
+import shared.model.RunnableWithFramePeriod;
+import visualiser.Commands.VisualiserRaceCommands.VisualiserRaceCommandFactory;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * The controller for race related messages, coming from the server to the client.
+ */
+public class VisualiserRaceController implements RunnableWithFramePeriod {
+
+
+ /**
+ * Incoming messages from server.
+ */
+ private BlockingQueue incomingMessages;
+
+
+ /**
+ * Commands are placed in here, and executed by visualiserRace.
+ */
+ private CompositeCommand compositeRaceCommand;
+
+
+ /**
+ * The context that created commands operate on.
+ */
+ private VisualiserRaceState visualiserRace;
+
+
+
+
+ /**
+ * Constructs a visualiserInput to convert an incoming stream of messages into commands.
+ * @param incomingMessages The incoming queue of messages.
+ * @param visualiserRace The context to for commands to operate on.
+ * @param compositeRaceCommand The composite command to place command in.
+ */
+ public VisualiserRaceController(BlockingQueue incomingMessages, VisualiserRaceState visualiserRace, CompositeCommand compositeRaceCommand) {
+ this.incomingMessages = incomingMessages;
+ this.compositeRaceCommand = compositeRaceCommand;
+ this.visualiserRace = visualiserRace;
+ }
+
+
+
+
+ @Override
+ public void run() {
+
+ while (!Thread.interrupted()) {
+
+ try {
+ AC35Data message = incomingMessages.take();
+
+ Command command = VisualiserRaceCommandFactory.create(message, visualiserRace);
+ compositeRaceCommand.addCommand(command);
+
+ } catch (CommandConstructionException e) {
+ Logger.getGlobal().log(Level.WARNING, "VisualiserRaceController could not create a command for incoming message.");
+
+ } catch (InterruptedException e) {
+ Logger.getGlobal().log(Level.SEVERE, "VisualiserRaceController was interrupted on thread: " + Thread.currentThread() + " while waiting for messages.");
+ Thread.currentThread().interrupt();
+ return;
+ }
+
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceEvent.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceEvent.java
new file mode 100644
index 00000000..d5ab9b66
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceEvent.java
@@ -0,0 +1,119 @@
+package visualiser.model;
+
+
+import javafx.beans.property.IntegerProperty;
+import mock.model.commandFactory.CompositeCommand;
+import network.Messages.Enums.RequestToJoinEnum;
+import shared.dataInput.EmptyBoatDataSource;
+import shared.dataInput.EmptyRaceDataSource;
+import shared.dataInput.EmptyRegattaDataSource;
+import visualiser.gameController.ControllerClient;
+import visualiser.network.ServerConnection;
+
+import java.io.IOException;
+import java.net.Socket;
+
+
+/**
+ * This class holds a race, and a client's connection to it
+ */
+public class VisualiserRaceEvent {
+
+ /**
+ * Our connection to the server.
+ */
+ private ServerConnection serverConnection;
+ /**
+ * The thread serverConnection is running on.
+ */
+ private Thread serverConnectionThread;
+
+
+
+ /**
+ * The race object which describes the currently occurring race.
+ */
+ private VisualiserRaceState visualiserRaceState;
+
+
+ /**
+ * The service for updating the {@link #visualiserRaceState}.
+ */
+ private VisualiserRaceService visualiserRaceService;
+ /**
+ * The thread {@link #visualiserRaceService} is running on.
+ */
+ private Thread visualiserRaceServiceThread;
+
+
+
+ /**
+ * Creates a visualiser race event, with a given socket and request type.
+ * @param socket The socket to connect to.
+ * @param requestType The type of {@link network.Messages.RequestToJoin} to make.
+ * @throws IOException Thrown if there is a problem with the socket.
+ */
+ public VisualiserRaceEvent(Socket socket, RequestToJoinEnum requestType) throws IOException {
+
+ this.visualiserRaceState = new VisualiserRaceState(new EmptyRaceDataSource(), new EmptyRegattaDataSource(), new EmptyBoatDataSource());
+
+
+ CompositeCommand raceCommands = new CompositeCommand();
+ this.visualiserRaceService = new VisualiserRaceService(raceCommands, visualiserRaceState);
+
+ this.visualiserRaceServiceThread = new Thread(visualiserRaceService, "VisualiserRaceEvent()->VisualiserRaceService thread " + visualiserRaceService);
+ this.visualiserRaceServiceThread.start();
+
+
+ this.serverConnection = new ServerConnection(socket, visualiserRaceState, raceCommands, requestType);
+ this.serverConnectionThread = new Thread(serverConnection, "StartController.enterLobby()->serverConnection thread " + serverConnection);
+ this.serverConnectionThread.start();
+
+
+ }
+
+
+ /**
+ * Returns the state of the race.
+ * @return The state of the race.
+ */
+ public VisualiserRaceState getVisualiserRaceState() {
+ return visualiserRaceState;
+ }
+
+
+ /**
+ * Returns the controller client, which writes BoatAction messages to the outgoing queue.
+ * @return The ControllerClient.
+ */
+ public ControllerClient getControllerClient() {
+ return serverConnection.getControllerClient();
+ }
+
+ /**
+ * Returns the connection to server.
+ * @return Connection to server.
+ */
+ public ServerConnection getServerConnection() {
+ return serverConnection;
+ }
+
+ /**
+ * Returns the framerate property of the race.
+ * @return Framerate property of race.
+ */
+ public IntegerProperty getFrameRateProperty() {
+ return visualiserRaceService.getFrameRateProperty();
+ }
+
+
+
+ /**
+ * Terminates the server connection and race service.
+ */
+ public void terminate() {
+ this.serverConnectionThread.interrupt();
+
+ this.visualiserRaceServiceThread.interrupt();
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceService.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceService.java
new file mode 100644
index 00000000..21be26d9
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceService.java
@@ -0,0 +1,93 @@
+package visualiser.model;
+
+import javafx.beans.property.IntegerProperty;
+import mock.model.commandFactory.CompositeCommand;
+import shared.model.FrameRateTracker;
+import shared.model.RunnableWithFramePeriod;
+
+
+/**
+ * Handles updating a {@link VisualiserRaceState} with incoming commands.
+ */
+public class VisualiserRaceService implements RunnableWithFramePeriod {
+
+
+ /**
+ * The race state to update.
+ */
+ private VisualiserRaceState visualiserRaceState;
+
+
+ /**
+ * A composite commands to execute to update the race.
+ */
+ private CompositeCommand raceCommands;
+
+
+ /**
+ * Used to track the framerate of the "simulation".
+ */
+ private FrameRateTracker frameRateTracker;
+
+
+
+
+
+
+ /**
+ * Constructs a visualiser race which models a yacht race, and is modified by CompositeCommand.
+ * @param raceCommands A composite commands to execute to update the race.
+ * @param visualiserRaceState The race state to update.
+ */
+ public VisualiserRaceService(CompositeCommand raceCommands, VisualiserRaceState visualiserRaceState) {
+ this.raceCommands = raceCommands;
+ this.visualiserRaceState = visualiserRaceState;
+
+ this.frameRateTracker = new FrameRateTracker();
+ }
+
+
+
+ /**
+ * Returns the CompositeCommand executed by the race.
+ * @return CompositeCommand executed by race.
+ */
+ public CompositeCommand getRaceCommands() {
+ return raceCommands;
+ }
+
+
+
+
+
+ @Override
+ public void run() {
+
+ long previousFrameTime = System.currentTimeMillis();
+
+ while (!Thread.interrupted()) {
+
+ long currentFrameTime = System.currentTimeMillis();
+
+ waitForFramePeriod(previousFrameTime, currentFrameTime, 16);
+
+ previousFrameTime = currentFrameTime;
+
+
+ raceCommands.execute();
+
+ }
+
+ frameRateTracker.stop();
+
+ }
+
+
+ /**
+ * Returns the framerate property of the race.
+ * @return Framerate property of race.
+ */
+ public IntegerProperty getFrameRateProperty() {
+ return frameRateTracker.fpsProperty();
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java
new file mode 100644
index 00000000..b1767cd5
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceState.java
@@ -0,0 +1,408 @@
+package visualiser.model;
+
+
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.paint.Color;
+import network.Messages.Enums.BoatStatusEnum;
+import shared.dataInput.BoatDataSource;
+import shared.dataInput.RaceDataSource;
+import shared.dataInput.RegattaDataSource;
+import shared.exceptions.BoatNotFoundException;
+import shared.exceptions.MarkNotFoundException;
+import shared.model.*;
+
+import java.time.Duration;
+import java.util.*;
+
+
+/**
+ * This class contains all of the state of a race on the client (visualiser) side.
+ */
+public class VisualiserRaceState extends RaceState {
+
+
+ /**
+ * A list of boats in the race.
+ */
+ private ObservableList boats;
+
+ /**
+ * The source ID of the boat assigned to the player.
+ * 0 if no boat has been assigned.
+ */
+ private int playerBoatID;
+
+
+ /**
+ * Maps between a Leg to a list of boats, in the order that they finished the leg.
+ * Used by the Sparkline to ensure it has correct information.
+ * TODO BUG: if we receive a race.xml file during the race, then we need to add/remove legs to this, without losing information.
+ */
+ private Map> legCompletionOrder;
+
+
+
+
+ /**
+ * An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor.
+ */
+ private List unassignedColors = new ArrayList<>(Arrays.asList(
+ Color.BLUEVIOLET,
+ Color.BLACK,
+ Color.RED,
+ Color.ORANGE,
+ Color.DARKOLIVEGREEN,
+ Color.LIMEGREEN,
+ Color.PURPLE,
+ Color.DARKGRAY,
+ Color.YELLOW
+ //TODO may need to add more colors.
+ ));
+
+
+ /**
+ * Constructs a visualiser race which models a yacht race.
+ * @param raceDataSource The raceDataSource to initialise with.
+ * @param regattaDataSource The regattaDataSource to initialise with.
+ * @param boatDataSource The boatDataSource to initialise with.
+ */
+ public VisualiserRaceState(RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, BoatDataSource boatDataSource) {
+
+ this.boats = FXCollections.observableArrayList();
+
+ this.playerBoatID = 0;
+
+ this.legCompletionOrder = new HashMap<>();
+
+
+ setRaceDataSource(raceDataSource);
+ setRegattaDataSource(regattaDataSource);
+ setBoatDataSource(boatDataSource);
+
+ }
+
+
+
+
+ /**
+ * Sets the race data source for this race to a new RaceDataSource.
+ * Uses the boundary and legs specified by the new RaceDataSource.
+ * @param raceDataSource The new RaceDataSource to use.
+ */
+ public void setRaceDataSource(RaceDataSource raceDataSource) {
+ super.setRaceDataSource(raceDataSource);
+
+ if (getBoatDataSource() != null) {
+ this.generateVisualiserBoats(this.boats, getBoatDataSource().getBoats(), raceDataSource.getParticipants(), unassignedColors);
+ }
+
+ initialiseLegCompletionOrder();
+ }
+
+ /**
+ * Sets the boat data source for this race to a new BoatDataSource.
+ * Uses the marker boats specified by the new BoatDataSource.
+ * @param boatDataSource The new BoatDataSource to use.
+ */
+ public void setBoatDataSource(BoatDataSource boatDataSource) {
+ super.setBoatDataSource(boatDataSource);
+
+ if (getRaceDataSource() != null) {
+ this.generateVisualiserBoats(this.boats, boatDataSource.getBoats(), getRaceDataSource().getParticipants(), unassignedColors);
+ }
+ }
+
+
+ /**
+ * Sets the regatta data source for this race to a new RegattaDataSource.
+ * @param regattaDataSource The new RegattaDataSource to use.
+ */
+ public void setRegattaDataSource(RegattaDataSource regattaDataSource) {
+ super.setRegattaDataSource(regattaDataSource);
+
+ }
+
+
+ /**
+ * Initialises the {@link #legCompletionOrder} map.
+ */
+ public void initialiseLegCompletionOrder() {
+ //Initialise the leg completion order map.
+ for (Leg leg : getLegs()) {
+ this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size()));
+ }
+ }
+
+
+
+ /**
+ * Generates a list of VisualiserBoats given a list of Boats, and a list of participating boats.
+ * This will add VisualiserBoats for newly participating sourceID, and remove VisualiserBoats for any participating sourceIDs that have been removed.
+ *
+ * @param existingBoats The visualiser boats that already exist in the race. This will be populated when we receive a new race.xml or boats.xml.
+ * @param boats The map of {@link Boat}s describing boats that are potentially in the race. Maps boat sourceID to boat.
+ * @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating.
+ * @param colors The list of unassignedColors to be used for the boats.
+ */
+ private void generateVisualiserBoats(ObservableList existingBoats, Map boats, List sourceIDs, List colors) {
+
+ //Remove any VisualiserBoats that are no longer participating.
+ for (VisualiserBoat boat : new ArrayList<>(existingBoats)) {
+
+ //Boat no longer is participating.
+ if (!sourceIDs.contains(boat.getSourceID())) {
+ //Return their colors to the color list.
+ colors.add(boat.getColor());
+
+ //Remove boat.
+ existingBoats.remove(boat);
+ }
+
+ }
+
+ //Get source IDs of already existing boats.
+ List existingBoatIDs = new ArrayList<>();
+ for (VisualiserBoat boat : existingBoats) {
+ existingBoatIDs.add(boat.getSourceID());
+ }
+
+ //Get source IDs of only newly participating boats.
+ List newBoatIDs = new ArrayList<>(sourceIDs);
+ newBoatIDs.removeAll(existingBoatIDs);
+
+ //Create VisualiserBoat for newly participating boats.
+ for (Integer sourceID : newBoatIDs) {
+
+ if (boats.containsKey(sourceID)) {
+
+ VisualiserBoat boat = new VisualiserBoat(
+ boats.get(sourceID),
+ colors.remove(colors.size() - 1));//TODO potential bug: not enough colors for boats.
+
+ boat.setCurrentLeg(getLegs().get(0));
+
+ existingBoats.add(boat);
+
+ }
+
+ }
+
+ setPlayerBoat();
+
+
+ }
+
+
+ /**
+ * Sets the boat the player has been assigned to as belonging to them.
+ */
+ private void setPlayerBoat() {
+
+ if (getPlayerBoatID() != 0) {
+
+ for (VisualiserBoat boat : new ArrayList<>(getBoats())) {
+
+ if (boat.getSourceID() == getPlayerBoatID()) {
+ boat.setClientBoat(true);
+ }
+
+ }
+
+ }
+
+ }
+
+
+ /**
+ * Initialise the boats in the race.
+ * This sets their current leg.
+ */
+ @Override
+ protected void initialiseBoats() {
+
+ Leg startingLeg = getLegs().get(0);
+
+ for (VisualiserBoat boat : boats) {
+
+ boat.setCurrentLeg(startingLeg);
+ boat.setTimeAtLastMark(getRaceClock().getCurrentTime());
+ boat.setCurrentPosition(new GPSCoordinate(0, 0));
+
+ }
+
+ }
+
+
+
+
+ /**
+ * Update position of boats in race (e.g, 5th), no position if on starting leg or DNF.
+ * @param boats The list of boats to update.
+ */
+ public void updateBoatPositions(List boats) {
+
+ //Sort boats.
+ sortBoatsByPosition(boats);
+
+ //Assign new positions.
+ for (int i = 0; i < boats.size(); i++) {
+ VisualiserBoat boat = boats.get(i);
+
+
+ if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) {
+
+ boat.setPosition("-");
+
+ } else {
+ boat.setPosition(Integer.toString(i + 1));
+ }
+ }
+
+ }
+
+
+ /**
+ * Sorts the list of boats by their position within the race.
+ * @param boats The list of boats in the race.
+ */
+ private void sortBoatsByPosition(List boats) {
+
+ boats.sort((a, b) -> {
+ //Get the difference in leg numbers.
+ int legNumberDelta = b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber();
+
+ //If they're on the same leg, we need to compare time to finish leg.
+ if (legNumberDelta == 0) {
+
+ //These are potentially null until we receive our first RaceStatus containing BoatStatuses.
+ if ((a.getEstimatedTimeAtNextMark() != null) && (b.getEstimatedTimeAtNextMark() != null)) {
+
+ return (int) Duration.between(
+ b.getEstimatedTimeAtNextMark(),
+ a.getEstimatedTimeAtNextMark() ).toMillis();
+
+ }
+
+ }
+
+ return legNumberDelta;
+
+ });
+
+ }
+
+
+
+
+ /**
+ * Returns the boats participating in the race.
+ * @return List of boats participating in the race.
+ */
+ public ObservableList getBoats() {
+ return boats;
+ }
+
+
+ /**
+ * Returns a boat by sourceID.
+ * @param sourceID The source ID the boat.
+ * @return The boat.
+ * @throws BoatNotFoundException Thrown if there is no boat with the specified sourceID.
+ */
+ public VisualiserBoat getBoat(int sourceID) throws BoatNotFoundException {
+
+ for (VisualiserBoat boat : boats) {
+
+ if (boat.getSourceID() == sourceID) {
+ return boat;
+ }
+
+ }
+
+ throw new BoatNotFoundException("Boat with sourceID: " + sourceID + " was not found.");
+ }
+
+ /**
+ * Returns whether or not there exists a {@link VisualiserBoat} with the given source ID.
+ * @param sourceID SourceID of VisualiserBoat.
+ * @return True if VisualiserBoat exists, false otherwise.
+ */
+ public boolean isVisualiserBoat(int sourceID) {
+
+ try {
+ getBoat(sourceID);
+ return true;
+ } catch (BoatNotFoundException e) {
+ return false;
+ }
+ }
+
+
+ /**
+ * Returns a mark by sourceID.
+ * @param sourceID The source ID the mark.
+ * @return The mark.
+ * @throws MarkNotFoundException Thrown if there is no mark with the specified sourceID.
+ */
+ public Mark getMark(int sourceID) throws MarkNotFoundException {
+
+ for (Mark mark : getMarks()) {
+
+ if (mark.getSourceID() == sourceID) {
+ return mark;
+ }
+
+ }
+
+ throw new MarkNotFoundException("Mark with sourceID: " + sourceID + " was not found.");
+ }
+
+
+ /**
+ * Returns whether or not there exists a {@link Mark} with the given source ID.
+ * @param sourceID SourceID of mark.
+ * @return True if mark exists, false otherwise.
+ */
+ public boolean isMark(int sourceID) {
+
+ try {
+ getMark(sourceID);
+ return true;
+ } catch (MarkNotFoundException e) {
+ return false;
+ }
+ }
+
+
+
+
+ /**
+ * Returns the order in which boats completed each leg. Maps the leg to a list of boats, ordered by the order in which they finished the leg.
+ * @return Leg completion order for each leg.
+ */
+ public Map> getLegCompletionOrder() {
+ return legCompletionOrder;
+ }
+
+
+ /**
+ * Gets the source ID of the player's boat. 0 if not assigned.
+ * @return Players boat source ID.
+ */
+ public int getPlayerBoatID() {
+ return playerBoatID;
+ }
+
+ /**
+ * sets the source ID of the player's boat. 0 if not assigned.
+ * @param playerBoatID Players boat source ID.
+ */
+ public void setPlayerBoatID(int playerBoatID) {
+ this.playerBoatID = playerBoatID;
+ setPlayerBoat();
+ }
+
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java b/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java
new file mode 100644
index 00000000..72d49c4a
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/network/ConnectionToServer.java
@@ -0,0 +1,173 @@
+package visualiser.network;
+
+
+import mock.model.commandFactory.Command;
+import network.Messages.AC35Data;
+import network.Messages.Enums.RequestToJoinEnum;
+import network.Messages.JoinAcceptance;
+import network.Messages.RequestToJoin;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import shared.model.RunnableWithFramePeriod;
+import visualiser.enums.ConnectionToServerState;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class tracks the state of the connection to a server.
+ */
+public class ConnectionToServer implements RunnableWithFramePeriod {
+
+
+ /**
+ * The state of the connection to the client.
+ */
+ private ConnectionToServerState connectionState = ConnectionToServerState.UNKNOWN;
+
+
+ /**
+ * The type of join request to make to server.
+ */
+ private RequestToJoinEnum requestType;
+
+
+ /**
+ * The queue to place outgoing messages on.
+ */
+ private BlockingQueue outgoingMessages;
+
+
+
+ /**
+ * The {@link JoinAcceptance} message that has been received, if any.
+ */
+ @Nullable
+ private JoinAcceptance joinAcceptance;
+
+
+ /**
+ * The incoming commands to execute.
+ */
+ private BlockingQueue incomingCommands;
+
+
+
+ /**
+ * Constructs a ConnectionToServer with a given state.
+ * @param connectionState The state of the connection.
+ * @param requestType The type of join request to make to server.
+ * @param incomingCommands The queue of commands to execute.
+ * @param outgoingMessages The queue to place outgoing messages on.
+ */
+ public ConnectionToServer(ConnectionToServerState connectionState, RequestToJoinEnum requestType, BlockingQueue incomingCommands, BlockingQueue outgoingMessages) {
+ this.connectionState = connectionState;
+ this.requestType = requestType;
+ this.incomingCommands = incomingCommands;
+ this.outgoingMessages = outgoingMessages;
+ }
+
+
+ /**
+ * Returns the state of this connection.
+ * @return The state of this connection.
+ */
+ public ConnectionToServerState getConnectionState() {
+ return connectionState;
+ }
+
+ /**
+ * Sets the state of this connection.
+ * @param connectionState The new state of this connection.
+ */
+ public void setConnectionState(ConnectionToServerState connectionState) {
+ this.connectionState = connectionState;
+ }
+
+
+ /**
+ * Returns the {@link JoinAcceptance} message received from the server, if any.
+ * @return The JoinAcceptance message from server. Null if no response from server.
+ */
+ @Nullable
+ public JoinAcceptance getJoinAcceptance() {
+ return joinAcceptance;
+ }
+
+ /**
+ * Sets the {@link JoinAcceptance} message received from the server, if any.
+ * @param joinAcceptance The new JoinAcceptance message from server.
+ */
+ public void setJoinAcceptance(@NotNull JoinAcceptance joinAcceptance) {
+ this.joinAcceptance = joinAcceptance;
+ }
+
+
+
+ @Override
+ public void run() {
+
+ try {
+ sendRequestToJoinMessage(requestType);
+
+ } catch (InterruptedException e) {
+ Logger.getGlobal().log(Level.SEVERE, "ConnectionToServer: " + this + " was interrupted on thread: " + Thread.currentThread() + " while sending RequestToJoin.", e);
+ Thread.currentThread().interrupt();
+
+ }
+
+ while (!Thread.interrupted()) {
+
+ try {
+ Command command = incomingCommands.take();
+ command.execute();
+
+ } catch (InterruptedException e) {
+ Logger.getGlobal().log(Level.SEVERE, "ConnectionToServer: " + this + " was interrupted on thread: " + Thread.currentThread() + " while reading command.", e);
+ Thread.currentThread().interrupt();
+ }
+
+ }
+
+ //If we get interrupted, we consider the connection to have timed-out.
+ connectionState = ConnectionToServerState.TIMED_OUT;
+
+ }
+
+
+
+ /**
+ * Sends the server a {@link RequestToJoin} message.
+ * @param requestType The type of request to send
+ * @throws InterruptedException Thrown if the thread is interrupted while placing message on the outgoing message queue.
+ */
+ private void sendRequestToJoinMessage(RequestToJoinEnum requestType) throws InterruptedException {
+
+ //Send them the source ID.
+ RequestToJoin requestToJoin = new RequestToJoin(requestType);
+
+ send(requestToJoin);
+
+ connectionState = ConnectionToServerState.REQUEST_SENT;
+ }
+
+
+ /**
+ * Sends a given message to the server, via the {@link #outgoingMessages} queue.
+ * @param message Message to send.
+ * @throws InterruptedException Thrown if thread is interrupted while sending message.
+ */
+ public void send(AC35Data message) throws InterruptedException {
+ outgoingMessages.put(message);
+ }
+
+
+ /**
+ * Returns the type of join request that was made.
+ * @return Type of join request made.
+ */
+ public RequestToJoinEnum getRequestType() {
+ return requestType;
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/network/ConnectionToServerController.java b/racevisionGame/src/main/java/visualiser/network/ConnectionToServerController.java
new file mode 100644
index 00000000..467bbd54
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/network/ConnectionToServerController.java
@@ -0,0 +1,78 @@
+package visualiser.network;
+
+
+import mock.exceptions.CommandConstructionException;
+import mock.model.commandFactory.Command;
+import network.Messages.AC35Data;
+import shared.model.RunnableWithFramePeriod;
+import visualiser.Commands.ConnectionToServerCommands.ConnectionToServerCommandFactory;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * The controller for connection related messages, coming from the server to the client.
+ */
+public class ConnectionToServerController implements RunnableWithFramePeriod {
+
+
+ /**
+ * The incoming queue of messages to act on.
+ */
+ private BlockingQueue incomingMessages;
+
+
+ /**
+ * The connection we are acting on.
+ */
+ private ConnectionToServer connectionToServer;
+
+
+ /**
+ * The queue to place commands on.
+ */
+ private BlockingQueue outgoingCommands;
+
+
+
+ /**
+ * Constructs a {@link ConnectionToServer} controller with the given parameters.
+ * This accepts connection related messages, converts them to commands, and passes them to an outgoing command queue.
+ * @param incomingMessages The message queue to read from.
+ * @param connectionToServer The ConnectionToServer (context) to act on.
+ * @param outgoingCommands The queue to place outgoing commands on.
+ */
+ public ConnectionToServerController(BlockingQueue incomingMessages, ConnectionToServer connectionToServer, BlockingQueue outgoingCommands) {
+ this.incomingMessages = incomingMessages;
+ this.connectionToServer = connectionToServer;
+ this.outgoingCommands = outgoingCommands;
+ }
+
+
+ @Override
+ public void run() {
+
+ while (!Thread.interrupted()) {
+
+ try {
+ AC35Data message = incomingMessages.take();
+ Command command = ConnectionToServerCommandFactory.create(message, connectionToServer);
+ outgoingCommands.put(command);
+
+ } catch (CommandConstructionException e) {
+ Logger.getGlobal().log(Level.WARNING, "ConnectionToServerController: " + this + " could not create command from message.", e);
+
+ } catch (InterruptedException e) {
+ Logger.getGlobal().log(Level.SEVERE, "ConnectionToServerController: " + this + " was interrupted on thread: " + Thread.currentThread(), e);
+ Thread.currentThread().interrupt();
+
+ }
+
+
+ }
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatController.java b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatController.java
new file mode 100644
index 00000000..739a3e64
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatController.java
@@ -0,0 +1,78 @@
+package visualiser.network;
+
+import mock.exceptions.CommandConstructionException;
+import mock.model.commandFactory.Command;
+import network.Messages.AC35Data;
+import shared.model.RunnableWithFramePeriod;
+import visualiser.Commands.IncomingHeartBeatCommands.IncomingHeartBeatCommandFactory;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * The controller for heartbeat related messages, coming from the server to the client.
+ */
+public class IncomingHeartBeatController implements RunnableWithFramePeriod {
+
+
+ /**
+ * The incoming queue of messages to act on.
+ */
+ private BlockingQueue incomingMessages;
+
+
+ /**
+ * The heart beat service we are acting on.
+ */
+ private IncomingHeartBeatService incomingHeartBeatService;
+
+
+ /**
+ * The queue to place commands on.
+ */
+ private BlockingQueue outgoingCommands;
+
+
+
+ /**
+ * Constructs a {@link IncomingHeartBeatService} controller with the given parameters.
+ * This accepts connection related messages, converts them to commands, and passes them to an outgoing command queue.
+ * @param incomingMessages The message queue to read from.
+ * @param incomingHeartBeatService The IncomingHeartBeatService (context) to act on.
+ * @param outgoingCommands The queue to place outgoing commands on.
+ */
+ public IncomingHeartBeatController(BlockingQueue incomingMessages, IncomingHeartBeatService incomingHeartBeatService, BlockingQueue outgoingCommands) {
+ this.incomingMessages = incomingMessages;
+ this.incomingHeartBeatService = incomingHeartBeatService;
+ this.outgoingCommands = outgoingCommands;
+ }
+
+
+
+ @Override
+ public void run() {
+
+ while (!Thread.interrupted()) {
+
+ try {
+ AC35Data message = incomingMessages.take();
+ Command command = IncomingHeartBeatCommandFactory.create(message, incomingHeartBeatService);
+ outgoingCommands.put(command);
+
+ } catch (CommandConstructionException e) {
+ Logger.getGlobal().log(Level.WARNING, "IncomingHeartBeatController: " + this + " could not create command from message.", e);
+
+ } catch (InterruptedException e) {
+ Logger.getGlobal().log(Level.SEVERE, "IncomingHeartBeatController: " + this + " was interrupted on thread: " + Thread.currentThread(), e);
+ Thread.currentThread().interrupt();
+
+ }
+
+
+ }
+
+ }
+
+}
diff --git a/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatService.java b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatService.java
new file mode 100644
index 00000000..e7c5b159
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/network/IncomingHeartBeatService.java
@@ -0,0 +1,109 @@
+package visualiser.network;
+
+import mock.model.commandFactory.Command;
+import shared.model.RunnableWithFramePeriod;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Tracks the heart beat status of a connection.
+ */
+public class IncomingHeartBeatService implements RunnableWithFramePeriod {
+
+
+ /**
+ * Timestamp of the last sent heartbeat message.
+ */
+ private long lastHeartbeatTime;
+
+
+ /**
+ * Sequence number for heartbeat messages.
+ */
+ private long lastHeartBeatSeqNum;
+
+
+ /**
+ * The incoming commands to execute.
+ */
+ private BlockingQueue incomingCommands;
+
+
+
+ /**
+ * Creates an {@link IncomingHeartBeatService} which executes commands from a given queue.
+ * @param incomingCommands Queue to read and execute commands from.
+ */
+ public IncomingHeartBeatService(BlockingQueue incomingCommands) {
+ this.incomingCommands = incomingCommands;
+
+ this.lastHeartbeatTime = System.currentTimeMillis();
+ this.lastHeartBeatSeqNum = -1;
+ }
+
+
+ /**
+ * Sets the last heart beat time to a given value.
+ * @param lastHeartbeatTime Timestamp of heartbeat.
+ */
+ public void setLastHeartbeatTime(long lastHeartbeatTime) {
+ this.lastHeartbeatTime = lastHeartbeatTime;
+ }
+
+ /**
+ * Sets the last heart beat sequence number to a given value.
+ * @param lastHeartBeatSeqNum Sequence number of heartbeat.
+ */
+ public void setLastHeartBeatSeqNum(long lastHeartBeatSeqNum) {
+ this.lastHeartBeatSeqNum = lastHeartBeatSeqNum;
+ }
+
+
+
+ /**
+ * Calculates the time since last heartbeat, in milliseconds.
+ *
+ * @return Time since last heartbeat, in milliseconds..
+ */
+ private long timeSinceHeartbeat() {
+ long now = System.currentTimeMillis();
+ return (now - lastHeartbeatTime);
+ }
+
+
+ /**
+ * Returns whether or not the heartBeat service considers the connection "alive".
+ * Going 10,000ms without receiving a heartBeat means that the connection is "dead".
+ * @return True if alive, false if dead.
+ */
+ public boolean isAlive() {
+ long heartBeatPeriod = 10000;
+
+ return (timeSinceHeartbeat() < heartBeatPeriod);
+ }
+
+
+
+ @Override
+ public void run() {
+
+
+ while (!Thread.interrupted()) {
+
+ try {
+ Command command = incomingCommands.take();
+ command.execute();
+
+ } catch (InterruptedException e) {
+ Logger.getGlobal().log(Level.SEVERE, "IncomingHeartBeatService: " + this + " was interrupted on thread: " + Thread.currentThread() + " while reading command.", e);
+ Thread.currentThread().interrupt();
+
+ }
+
+ }
+
+ }
+}
diff --git a/racevisionGame/src/main/java/visualiser/network/ServerConnection.java b/racevisionGame/src/main/java/visualiser/network/ServerConnection.java
new file mode 100644
index 00000000..ed540566
--- /dev/null
+++ b/racevisionGame/src/main/java/visualiser/network/ServerConnection.java
@@ -0,0 +1,477 @@
+package visualiser.network;
+
+
+import mock.model.commandFactory.Command;
+import mock.model.commandFactory.CompositeCommand;
+import network.MessageRouters.MessageRouter;
+import network.Messages.AC35Data;
+import network.Messages.Enums.MessageType;
+import network.Messages.Enums.RequestToJoinEnum;
+import network.Messages.LatestMessages;
+import network.StreamRelated.MessageDeserialiser;
+import network.StreamRelated.MessageSerialiser;
+import shared.model.RunnableWithFramePeriod;
+import visualiser.model.VisualiserRaceEvent;
+import visualiser.model.VisualiserRaceController;
+import visualiser.enums.ConnectionToServerState;
+import visualiser.gameController.ControllerClient;
+import visualiser.model.VisualiserRaceState;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This class handles the client-server connection handshake.
+ */
+public class ServerConnection implements RunnableWithFramePeriod {
+
+ /**
+ * The socket for the connection to server.
+ */
+ private Socket socket;
+
+
+
+
+
+ /**
+ * Used to send client input to server.
+ */
+ private ControllerClient controllerClient;
+
+
+
+ /**
+ * Used to write messages to socket.
+ */
+ private MessageSerialiser messageSerialiser;
+ /**
+ * The thread {@link #messageSerialiser} runs on.
+ */
+ private Thread messageSerialiserThread;
+
+ /**
+ * Used to read messages from socket.
+ */
+ private MessageDeserialiser messageDeserialiser;
+ /**
+ * The thread {@link #messageDeserialiser} runs on.
+ */
+ private Thread messageDeserialiserThread;
+
+
+
+
+ /**
+ * Router to route messages to correct queue.
+ */
+ private MessageRouter messageRouter;
+ /**
+ * The thread {@link #messageRouter} runs on.
+ */
+ private Thread messageRouterThread;
+
+
+
+ /**
+ * The state of the connection to the client.
+ */
+ private ConnectionToServer connectionToServer;
+ /**
+ * The thread {@link #connectionToServer} runs on.
+ */
+ private Thread connectionToServerThread;
+
+ /**
+ * The controller which handles JoinAcceptance messages.
+ */
+ private ConnectionToServerController connectionToServerController;
+ /**
+ * The thread {@link #connectionToServerController} runs on.
+ */
+ private Thread connectionToServerControllerThread;
+
+
+
+ /**
+ * Tracks the heartBeat status of the connection.
+ */
+ private IncomingHeartBeatService heartBeatService;
+ /**
+ * The thread {@link #heartBeatService} runs on.
+ */
+ private Thread heartBeatServiceThread;
+
+ /**
+ * Tracks the heartBeat status of the connection.
+ */
+ private IncomingHeartBeatController heartBeatController;
+ /**
+ * The thread {@link #heartBeatController} runs on.
+ */
+ private Thread heartBeatControllerThread;
+
+
+ /**
+ * This is the race we are modelling.
+ */
+ private VisualiserRaceState visualiserRaceState;
+
+ /**
+ * The CompositeCommand to place race commands in.
+ */
+ private CompositeCommand raceCommands;
+
+
+ /**
+ * Used to convert incoming messages into a race snapshot.
+ */
+ private VisualiserRaceController visualiserRaceController;
+ /**
+ * The thread {@link #visualiserRaceController} runs on.
+ */
+ private Thread visualiserRaceControllerThread;
+
+
+
+
+ /**
+ * Creates a server connection, using a given socket.
+ * @param socket The socket which connects to the client.
+ * @param visualiserRaceState The race for the {@link VisualiserRaceController} to send commands to.
+ * @param raceCommands The CompositeCommand to place race commands in.
+ * @param requestType The type of join request to make.
+ * @throws IOException Thrown if there is a problem with the client socket.
+ */
+ public ServerConnection(Socket socket, VisualiserRaceState visualiserRaceState, CompositeCommand raceCommands, RequestToJoinEnum requestType) throws IOException {
+ this.socket = socket;
+ this.visualiserRaceState = visualiserRaceState;
+ this.raceCommands = raceCommands;
+
+ createMessageSerialiser(socket);
+ createMessageDeserialiser(socket);
+
+ createRouter(messageDeserialiser.getMessagesRead());
+
+ createConnectionToServer(requestType);
+
+
+ messageRouterThread.start();
+
+
+ this.controllerClient = new ControllerClient(messageRouter.getIncomingMessageQueue());
+
+ }
+
+
+
+ /**
+ * Creates this connection's {@link MessageRouter}, and gives it a queue to read from.
+ * Does not start {@link #messageRouterThread}. Start it after setting up any initial routes.
+ * @param inputQueue Queue for the MessageRouter to read from.
+ */
+ private void createRouter(BlockingQueue inputQueue) {
+ this.messageRouter = new MessageRouter(inputQueue);
+
+ this.messageRouterThread = new Thread(messageRouter, "ServerConnection()->MessageRouter thread " + messageRouter);
+
+ //Unrouted messages get sent back to the router. Kind of ugly, but we do this to ensure that no messages are lost while initializing (e.g., XML message being received before setting up the route for it).
+ messageRouter.addDefaultRoute(messageRouter.getIncomingMessageQueue());
+ }
+
+
+ /**
+ * Creates the {@link #connectionToServer} and {@link #connectionToServerController}, and starts their threads.
+ * @param requestType The type of join request to make to server.
+ */
+ private void createConnectionToServer(RequestToJoinEnum requestType) {
+
+ //ConnectionToServer executes these commands.
+ BlockingQueue commands = new LinkedBlockingQueue<>();
+ this.connectionToServer = new ConnectionToServer(ConnectionToServerState.UNKNOWN, requestType, commands, messageRouter.getIncomingMessageQueue());
+
+ //ConnectionToServerController receives messages, and places commands on the above command queue.
+ BlockingQueue incomingJoinMessages = new LinkedBlockingQueue<>();
+ this.connectionToServerController = new ConnectionToServerController(incomingJoinMessages, connectionToServer, commands);
+
+ //Route JoinAcceptance messages to the controller, and RequestToJoin to the socket.
+ this.messageRouter.addRoute(MessageType.JOIN_ACCEPTANCE, incomingJoinMessages);
+ this.messageRouter.addRoute(MessageType.REQUEST_TO_JOIN, messageSerialiser.getMessagesToSend());
+
+
+ //Start the above on new threads.
+ this.connectionToServerThread = new Thread(connectionToServer, "ServerConnection()->ConnectionToServer thread " + connectionToServer);
+ this.connectionToServerThread.start();
+
+ this.connectionToServerControllerThread = new Thread(connectionToServerController,"ServerConnection()->ConnectionToServerController thread " + connectionToServerController);
+ this.connectionToServerControllerThread.start();
+ }
+
+ /**
+ * Removes connection message related routes from the router.
+ * This is called after the client-server connection is properly established, so that any future (erroneous) connection messages get ignored.
+ */
+ private void removeConnectionRoutes() {
+ this.messageRouter.removeRoute(MessageType.JOIN_ACCEPTANCE);
+ this.messageRouter.removeRoute(MessageType.REQUEST_TO_JOIN);
+ }
+
+
+ /**
+ * Creates the {@link #messageSerialiser} and starts its thread.
+ * @param socket The socket to write to.
+ * @throws IOException Thrown if we cannot get an outputStream from the socket
+ */
+ private void createMessageSerialiser(Socket socket) throws IOException {
+ BlockingQueue outputQueue = new LinkedBlockingQueue<>();
+ this.messageSerialiser = new MessageSerialiser(socket.getOutputStream(), outputQueue);
+
+ this.messageSerialiserThread = new Thread(messageSerialiser, "ServerConnection()->MessageSerialiser thread " + messageSerialiser);
+ this.messageSerialiserThread.start();
+ }
+
+ /**
+ * Creates the {@link #messageDeserialiser} and starts its thread.
+ * @param socket The socket to read from.
+ * @throws IOException Thrown if we cannot get an inputStream from the socket
+ */
+ private void createMessageDeserialiser(Socket socket) throws IOException {
+ BlockingQueue inputQueue = new LinkedBlockingQueue<>();
+ this.messageDeserialiser = new MessageDeserialiser(socket.getInputStream(), inputQueue);
+
+ this.messageDeserialiserThread = new Thread(messageDeserialiser, "ServerConnection()->MessageDeserialiser thread " + messageDeserialiser);
+ this.messageDeserialiserThread.start();
+ }
+
+
+ /**
+ * Creates the {@link #heartBeatService} and {@link #heartBeatController} and starts their threads.
+ */
+ private void createHeartBeatService() {
+
+ //IncomingHeartBeatService executes these commands.
+ BlockingQueue commands = new LinkedBlockingQueue<>();
+ this.heartBeatService = new IncomingHeartBeatService(commands);
+
+ //IncomingHeartBeatController receives messages, and places commands on the above command queue.
+ BlockingQueue incomingHeartBeatMessages = new LinkedBlockingQueue<>();
+ this.heartBeatController = new IncomingHeartBeatController(incomingHeartBeatMessages, heartBeatService, commands);
+
+ //Route HeartBeat messages to the controller.
+ this.messageRouter.addRoute(MessageType.HEARTBEAT, incomingHeartBeatMessages);
+
+
+ //Start the above on new threads.
+ this.heartBeatServiceThread = new Thread(heartBeatService, "ServerConnection()->IncomingHeartBeatService thread " + connectionToServer);
+ this.heartBeatServiceThread.start();
+
+ this.heartBeatControllerThread = new Thread(heartBeatController,"ServerConnection()->IncomingHeartBeatController thread " + connectionToServerController);
+ this.heartBeatControllerThread.start();
+
+ }
+
+
+ /**
+ * Creates the {@link #visualiserRaceController} and starts its thread.
+ */
+ private void createVisualiserRaceController() {
+
+
+ //VisualiserRaceController receives messages, and places commands on the race's command queue.
+ BlockingQueue incomingMessages = new LinkedBlockingQueue<>();
+ this.visualiserRaceController = new VisualiserRaceController(incomingMessages, visualiserRaceState, raceCommands);
+
+
+ //Routes.
+ this.messageRouter.addRoute(MessageType.BOATLOCATION, incomingMessages);
+ this.messageRouter.addRoute(MessageType.RACESTATUS, incomingMessages);
+ this.messageRouter.addRoute(MessageType.RACESTARTSTATUS, incomingMessages);
+ this.messageRouter.addRoute(MessageType.AVGWIND, incomingMessages);
+ this.messageRouter.addRoute(MessageType.COURSEWIND, incomingMessages);
+ this.messageRouter.addRoute(MessageType.CHATTERTEXT, incomingMessages);
+ this.messageRouter.addRoute(MessageType.DISPLAYTEXTMESSAGE, incomingMessages);
+ this.messageRouter.addRoute(MessageType.YACHTACTIONCODE, incomingMessages);
+ this.messageRouter.addRoute(MessageType.YACHTEVENTCODE, incomingMessages);
+ this.messageRouter.addRoute(MessageType.MARKROUNDING, incomingMessages);
+ this.messageRouter.addRoute(MessageType.XMLMESSAGE, incomingMessages);
+ this.messageRouter.addRoute(MessageType.ASSIGN_PLAYER_BOAT, incomingMessages);
+ this.messageRouter.removeDefaultRoute(); //We no longer want to keep un-routed messages.
+
+
+ //Start the above on a new thread.
+
+ this.visualiserRaceControllerThread = new Thread(visualiserRaceController, "ServerConnection()->VisualiserRaceController thread " + visualiserRaceController);
+ this.visualiserRaceControllerThread.start();
+
+ }
+
+
+ //TODO create input controller here. RaceController should query for it, if it exists.
+ private void createPlayerInputController() {
+
+ this.messageRouter.addRoute(MessageType.BOATACTION, messageSerialiser.getMessagesToSend());
+ //TODO routes
+ }
+
+
+
+
+ @Override
+ public void run() {
+
+ //Monitor the connection state.
+
+ long previousFrameTime = System.currentTimeMillis();
+
+ while (!Thread.interrupted()) {
+
+ long currentFrameTime = System.currentTimeMillis();
+ waitForFramePeriod(previousFrameTime, currentFrameTime, 100);
+ previousFrameTime = currentFrameTime;
+
+
+ ConnectionToServerState state = connectionToServer.getConnectionState();
+
+ switch (state) {
+
+ case CONNECTED:
+ connected();
+ break;
+
+ case DECLINED:
+ declined();
+ break;
+
+ case TIMED_OUT:
+ timedOut();
+ break;
+
+ }
+
+ }
+
+ }
+
+
+
+ /**
+ * Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#CONNECTED}.
+ */
+ private void connected() {
+
+ createHeartBeatService();
+
+ createVisualiserRaceController();
+
+ if (connectionToServer.getRequestType() == RequestToJoinEnum.PARTICIPANT) {
+ createPlayerInputController();
+ }
+
+
+ //We no longer want connection messages to be accepted.
+ removeConnectionRoutes();
+
+
+ //We interrupt as this thread's run() isn't needed anymore.
+ Thread.currentThread().interrupt();
+ }
+
+
+ /**
+ * Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#DECLINED}.
+ */
+ private void declined() {
+ Logger.getGlobal().log(Level.WARNING, "Server handshake failed. Connection was declined.");
+
+ terminate();
+
+ Thread.currentThread().interrupt();
+ }
+
+
+ /**
+ * Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#TIMED_OUT}.
+ */
+ private void timedOut() {
+ Logger.getGlobal().log(Level.WARNING, "Server handshake failed. Connection timed out.");
+
+ terminate();
+
+ Thread.currentThread().interrupt();
+ }
+
+
+
+
+ /**
+ * Determines whether or not this connection is still alive.
+ * This is based off whether the {@link #messageDeserialiser}, {@link #messageSerialiser}, and {@link #heartBeatService} are alive.
+ * @return True if it is alive, false otherwise.
+ */
+ public boolean isAlive() {
+ return messageDeserialiser.isRunning() && messageSerialiser.isRunning() && heartBeatService.isAlive();
+ }
+
+
+ /**
+ * Returns the controller client, which writes BoatAction messages to the outgoing queue.
+ * @return The ControllerClient.
+ */
+ public ControllerClient getControllerClient() {
+ return controllerClient;
+ }
+
+
+
+
+ /**
+ * Terminates the connection and any running threads.
+ */
+ public void terminate() {
+
+ if (this.messageRouterThread != null) {
+ this.messageRouterThread.interrupt();
+ }
+
+
+ if (this.messageSerialiserThread != null) {
+ this.messageSerialiserThread.interrupt();
+ }
+ if (this.messageDeserialiserThread != null) {
+ this.messageDeserialiserThread.interrupt();
+ }
+
+
+
+ if (this.connectionToServerThread != null) {
+ this.connectionToServerThread.interrupt();
+ }
+ if (this.connectionToServerControllerThread != null) {
+ this.connectionToServerControllerThread.interrupt();
+ }
+
+
+ if (this.heartBeatServiceThread != null) {
+ this.heartBeatServiceThread.interrupt();
+ }
+ if (this.heartBeatControllerThread != null) {
+ this.heartBeatControllerThread.interrupt();
+ }
+
+
+ if (this.visualiserRaceControllerThread != null) {
+ this.visualiserRaceControllerThread.interrupt();
+ }
+
+
+
+ //TODO input controller?
+
+ }
+
+
+}
diff --git a/racevisionGame/src/main/resources/css/dayMode.css b/racevisionGame/src/main/resources/css/dayMode.css
index b15f242b..d0f62fb7 100644
--- a/racevisionGame/src/main/resources/css/dayMode.css
+++ b/racevisionGame/src/main/resources/css/dayMode.css
@@ -50,4 +50,6 @@
.scroll-bar > .increment-button:pressed > .increment-arrow,
.scroll-bar > .decrement-button:pressed > .decrement-arrow {
-fx-background-color: -fx-mark-highlight-color, rgb(255, 255, 255);
-}
\ No newline at end of file
+}
+
+
diff --git a/racevisionGame/src/main/resources/css/nightMode.css b/racevisionGame/src/main/resources/css/nightMode.css
index 406cc60b..7fe6a67b 100644
--- a/racevisionGame/src/main/resources/css/nightMode.css
+++ b/racevisionGame/src/main/resources/css/nightMode.css
@@ -51,4 +51,9 @@
.scroll-bar > .increment-button:pressed > .increment-arrow,
.scroll-bar > .decrement-button:pressed > .decrement-arrow {
-fx-background-color: -fx-mark-highlight-color, rgb(255, 255, 255);
-}
\ No newline at end of file
+}
+
+
+#arrowImage {
+ -fx-image: url("/visualiser/images/arrowLight.png");
+}
diff --git a/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml b/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml
index b43abe7f..9295dc07 100644
--- a/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml
+++ b/racevisionGame/src/main/resources/mock/mockXML/boatTest.xml
@@ -33,8 +33,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/racevisionGame/src/main/resources/mock/mockXML/boatsSinglePlayer.xml b/racevisionGame/src/main/resources/mock/mockXML/boatsSinglePlayer.xml
index 98058f3d..e783821f 100644
--- a/racevisionGame/src/main/resources/mock/mockXML/boatsSinglePlayer.xml
+++ b/racevisionGame/src/main/resources/mock/mockXML/boatsSinglePlayer.xml
@@ -36,20 +36,5 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml b/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml
new file mode 100644
index 00000000..553c2571
--- /dev/null
+++ b/racevisionGame/src/main/resources/mock/mockXML/raceSinglePlayer.xml
@@ -0,0 +1,52 @@
+
+
+ 5326
+ FLEET
+ CREATION_TIME
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml b/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml
index 83e36f85..edd634dc 100644
--- a/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml
+++ b/racevisionGame/src/main/resources/mock/mockXML/raceTest.xml
@@ -5,15 +5,20 @@
CREATION_TIME
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -49,4 +54,4 @@
-
\ No newline at end of file
+
diff --git a/racevisionGame/src/main/resources/visualiser/images/arrowLight.png b/racevisionGame/src/main/resources/visualiser/images/arrowLight.png
new file mode 100644
index 00000000..7a80459d
Binary files /dev/null and b/racevisionGame/src/main/resources/visualiser/images/arrowLight.png differ
diff --git a/racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml b/racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml
index 6e8a88b5..4057753d 100644
--- a/racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml
+++ b/racevisionGame/src/main/resources/visualiser/scenes/arrow.fxml
@@ -1,34 +1,58 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
-
+
diff --git a/racevisionGame/src/main/resources/visualiser/scenes/connect.fxml b/racevisionGame/src/main/resources/visualiser/scenes/connect.fxml
index f620bfeb..4308a47f 100644
--- a/racevisionGame/src/main/resources/visualiser/scenes/connect.fxml
+++ b/racevisionGame/src/main/resources/visualiser/scenes/connect.fxml
@@ -1,13 +1,18 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/racevisionGame/src/main/resources/visualiser/scenes/lobby.fxml b/racevisionGame/src/main/resources/visualiser/scenes/lobby.fxml
index 13159bec..2f84063e 100644
--- a/racevisionGame/src/main/resources/visualiser/scenes/lobby.fxml
+++ b/racevisionGame/src/main/resources/visualiser/scenes/lobby.fxml
@@ -1,13 +1,18 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
+
@@ -35,31 +40,52 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml
index 76da5379..718e65ff 100644
--- a/racevisionGame/src/main/resources/visualiser/scenes/race.fxml
+++ b/racevisionGame/src/main/resources/visualiser/scenes/race.fxml
@@ -1,12 +1,32 @@
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
@@ -16,7 +36,6 @@
-
@@ -31,16 +50,17 @@
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
@@ -76,7 +96,11 @@
-
+
+
+
+
+
@@ -84,8 +108,8 @@
-
-
+
+
diff --git a/racevisionGame/src/test/java/mock/dataInput/PolarParserTest.java b/racevisionGame/src/test/java/mock/dataInput/PolarParserTest.java
index 78d3364f..b051ba79 100644
--- a/racevisionGame/src/test/java/mock/dataInput/PolarParserTest.java
+++ b/racevisionGame/src/test/java/mock/dataInput/PolarParserTest.java
@@ -14,6 +14,13 @@ import static org.junit.Assert.fail;
public class PolarParserTest {
+
+ public static Polars createPolars() {
+ Polars polars = PolarParser.parse("mock/polars/acc_polars.csv");
+ return polars;
+ }
+
+
/**
* Tests if we can parse a valid polar data file (stored in a string), and create a polar table.
*/
diff --git a/racevisionGame/src/test/java/mock/model/ConstantWindGeneratorTest.java b/racevisionGame/src/test/java/mock/model/ConstantWindGeneratorTest.java
new file mode 100644
index 00000000..6f67dc30
--- /dev/null
+++ b/racevisionGame/src/test/java/mock/model/ConstantWindGeneratorTest.java
@@ -0,0 +1,72 @@
+package mock.model;
+
+import org.junit.Before;
+import org.junit.Test;
+import shared.model.Bearing;
+import shared.model.Wind;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests the {@link ConstantWindGenerator}.
+ */
+public class ConstantWindGeneratorTest {
+
+ WindGenerator windGenerator;
+ Bearing windBearing;
+ double windSpeedKnots;
+
+ @Before
+ public void setUp() throws Exception {
+
+ windBearing = Bearing.fromDegrees(78.5);
+ windSpeedKnots = 18.54;
+
+ windGenerator = new ConstantWindGenerator(windBearing, windSpeedKnots);
+
+ }
+
+
+ /**
+ * Tests if the {@link WindGenerator#generateBaselineWind()} function works.
+ */
+ @Test
+ public void generateBaselineTest() {
+
+ int repetitions = 100;
+
+ for (int i = 0; i < repetitions; i++) {
+
+ Wind wind = windGenerator.generateBaselineWind();
+
+ assertEquals(windBearing.degrees(), wind.getWindDirection().degrees(), 0.01);
+ assertEquals(windSpeedKnots, wind.getWindSpeed(), 0.01);
+
+ }
+
+ }
+
+
+ /**
+ * Tests if the {@link WindGenerator#generateNextWind(Wind)} ()} function works.
+ */
+ @Test
+ public void generateNextWindTest() {
+
+ int repetitions = 100;
+
+ Wind wind = windGenerator.generateBaselineWind();
+
+ for (int i = 0; i < repetitions; i++) {
+
+ wind = windGenerator.generateNextWind(wind);
+
+ assertEquals(windBearing.degrees(), wind.getWindDirection().degrees(), 0.01);
+ assertEquals(windSpeedKnots, wind.getWindSpeed(), 0.01);
+
+ }
+
+ }
+
+}
diff --git a/racevisionGame/src/test/java/mock/model/MockRaceTest.java b/racevisionGame/src/test/java/mock/model/MockRaceTest.java
index cf530665..4f3f7705 100644
--- a/racevisionGame/src/test/java/mock/model/MockRaceTest.java
+++ b/racevisionGame/src/test/java/mock/model/MockRaceTest.java
@@ -1,7 +1,43 @@
package mock.model;
+import mock.dataInput.PolarParserTest;
+import network.Messages.LatestMessages;
+import shared.dataInput.*;
+import shared.exceptions.InvalidBoatDataException;
+import shared.exceptions.InvalidRaceDataException;
+import shared.exceptions.InvalidRegattaDataException;
+import shared.model.Bearing;
+import shared.model.Constants;
+
import static org.junit.Assert.*;
public class MockRaceTest {
//TODO
+
+
+ /**
+ * Creates a MockRace for use in testing.
+ * Has a constant wind generator.
+ * @return MockRace for testing.
+ * @throws InvalidBoatDataException If the BoatDataSource cannot be created.
+ * @throws InvalidRaceDataException If the RaceDataSource cannot be created.
+ * @throws InvalidRegattaDataException If the RegattaDataSource cannot be created.
+ */
+ public static MockRace createMockRace() throws InvalidBoatDataException, InvalidRaceDataException, InvalidRegattaDataException {
+
+ BoatDataSource boatDataSource = BoatXMLReaderTest.createBoatDataSource();
+ RaceDataSource raceDataSource = RaceXMLReaderTest.createRaceDataSource();
+ RegattaDataSource regattaDataSource = RegattaXMLReaderTest.createRegattaDataSource();
+
+
+ Polars polars = PolarParserTest.createPolars();
+
+ WindGenerator windGenerator = new ConstantWindGenerator(Bearing.fromDegrees(230), 10);
+
+ MockRace mockRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, polars, Constants.RaceTimeScale, windGenerator);
+
+
+ return mockRace;
+
+ }
}
diff --git a/racevisionGame/src/test/java/mock/model/RandomWindGeneratorTest.java b/racevisionGame/src/test/java/mock/model/RandomWindGeneratorTest.java
new file mode 100644
index 00000000..76eed977
--- /dev/null
+++ b/racevisionGame/src/test/java/mock/model/RandomWindGeneratorTest.java
@@ -0,0 +1,111 @@
+package mock.model;
+
+import org.junit.Before;
+import org.junit.Test;
+import shared.model.Bearing;
+import shared.model.Wind;
+
+import static org.junit.Assert.*;
+
+
+public class RandomWindGeneratorTest {
+
+
+ private RandomWindGenerator randomWindGenerator;
+
+ private Bearing windBaselineBearing;
+ private Bearing windBearingLowerBound;
+ private Bearing windBearingUpperBound;
+ private double windBaselineSpeed;
+ private double windSpeedLowerBound;
+ private double windSpeedUpperBound;
+
+ private double speedKnotsEpsilon;
+ private double bearingDegreeEpsilon;
+
+
+ @Before
+ public void setUp() throws Exception {
+
+
+ //Bounds.
+ this.windBaselineBearing = Bearing.fromDegrees(88.3);
+ this.windBearingLowerBound = Bearing.fromDegrees(66.5);
+ this.windBearingUpperBound = Bearing.fromDegrees(248.6);
+ this.windBaselineSpeed = 13;
+ this.windSpeedLowerBound = 7;
+ this.windSpeedUpperBound = 20;
+
+ this.randomWindGenerator = new RandomWindGenerator(
+ windBaselineBearing,
+ windBearingLowerBound,
+ windBearingUpperBound,
+ windBaselineSpeed,
+ windSpeedLowerBound,
+ windSpeedUpperBound );
+
+ this.bearingDegreeEpsilon = 0.001;
+ this.speedKnotsEpsilon = 0.001;
+ }
+
+
+ /**
+ * Tests if the baseline wind generated it accurate.
+ */
+ @Test
+ public void generateBaselineWindTest() {
+
+ Wind wind = randomWindGenerator.generateBaselineWind();
+
+ assertEquals(windBaselineSpeed, wind.getWindSpeed(), speedKnotsEpsilon);
+ assertEquals(windBaselineBearing.degrees(), wind.getWindDirection().degrees(), bearingDegreeEpsilon);
+
+ }
+
+ /**
+ * Tests if the random wind generated is inside the bounds.
+ */
+ @Test
+ public void generateRandomWindTest() {
+
+ int randomWindCount = 1000;
+
+ for (int i = 0; i < randomWindCount; i++) {
+
+ Wind wind = randomWindGenerator.generateRandomWind();
+
+ assertTrue(wind.getWindSpeed() >= windSpeedLowerBound);
+ assertTrue(wind.getWindSpeed() <= windSpeedUpperBound);
+
+ assertTrue(wind.getWindDirection().degrees() >= windBearingLowerBound.degrees());
+ assertTrue(wind.getWindDirection().degrees() <= windBearingUpperBound.degrees());
+
+ }
+
+ }
+
+
+ /**
+ * Tests if the next wind generated is inside the bounds.
+ */
+ @Test
+ public void generateNextWindTest() {
+
+ Wind wind = randomWindGenerator.generateBaselineWind();
+
+ int randomWindCount = 1000;
+
+ for (int i = 0; i < randomWindCount; i++) {
+
+ wind = randomWindGenerator.generateNextWind(wind);
+
+ assertTrue(wind.getWindSpeed() >= windSpeedLowerBound);
+ assertTrue(wind.getWindSpeed() <= windSpeedUpperBound);
+
+ assertTrue(wind.getWindDirection().degrees() >= windBearingLowerBound.degrees());
+ assertTrue(wind.getWindDirection().degrees() <= windBearingUpperBound.degrees());
+
+ }
+
+ }
+}
diff --git a/racevisionGame/src/test/java/mock/model/SourceIdAllocatorTest.java b/racevisionGame/src/test/java/mock/model/SourceIdAllocatorTest.java
new file mode 100644
index 00000000..7240e01b
--- /dev/null
+++ b/racevisionGame/src/test/java/mock/model/SourceIdAllocatorTest.java
@@ -0,0 +1,126 @@
+package mock.model;
+
+import mock.exceptions.SourceIDAllocationException;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests if allocating source IDs works.
+ */
+public class SourceIdAllocatorTest {
+
+ /**
+ * This is the list of source IDs that we start with.
+ */
+ private List originalSourceIDs;
+
+ /**
+ * Used to allocate source IDs.
+ */
+ private SourceIdAllocator sourceIdAllocator;
+
+
+ @Before
+ public void setUp() throws Exception {
+
+ originalSourceIDs = new ArrayList<>();
+ originalSourceIDs.add(120);
+ originalSourceIDs.add(121);
+ originalSourceIDs.add(122);
+ originalSourceIDs.add(123);
+ originalSourceIDs.add(124);
+ originalSourceIDs.add(125);
+
+
+ sourceIdAllocator = new SourceIdAllocator(originalSourceIDs);
+
+ }
+
+
+ /**
+ * Tests that allocation fails when we don't have any source IDs to allocate.
+ */
+ @Test
+ public void emptyAllocationTest() {
+
+ SourceIdAllocator allocator = new SourceIdAllocator(new ArrayList<>());
+
+
+ try {
+ int sourceID = allocator.allocateSourceID();
+
+ fail("Exception should have been thrown, but wasn't.");
+
+ } catch (SourceIDAllocationException e) {
+
+ //We expect this exception to be thrown - success.
+
+ }
+
+ }
+
+
+ /**
+ * Tests that we can allocate a source ID.
+ * @throws Exception Thrown in case of error.
+ */
+ @Test
+ public void allocationTest() throws Exception {
+
+
+ int sourceID = sourceIdAllocator.allocateSourceID();
+
+ }
+
+
+ /**
+ * Tests that we can allocate source IDs, but it will eventually be unable to allocate source IDs.
+ */
+ @Test
+ public void allocationEventuallyFailsTest() {
+
+ while (true) {
+
+ try {
+ int sourceID = sourceIdAllocator.allocateSourceID();
+
+ } catch (SourceIDAllocationException e) {
+ //We expect to encounter this exception after enough allocations - success.
+ break;
+
+ }
+
+ }
+
+ }
+
+
+ /**
+ * Tests if we can allocate a source ID, return it, and reallocate it.
+ * @throws Exception Thrown in case of error.
+ */
+ @Test
+ public void reallocationTest() throws Exception {
+
+ List sourceIDList = new ArrayList<>();
+ sourceIDList.add(123);
+
+ SourceIdAllocator sourceIdAllocator = new SourceIdAllocator(sourceIDList);
+
+ //Allocate.
+ int sourceID = sourceIdAllocator.allocateSourceID();
+
+ //Return.
+ sourceIdAllocator.returnSourceID(sourceID);
+
+ //Reallocate.
+ int sourceID2 = sourceIdAllocator.allocateSourceID();
+
+ }
+}
diff --git a/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java b/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java
index e5d147d9..2193eb7a 100644
--- a/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java
+++ b/racevisionGame/src/test/java/mock/model/commandFactory/WindCommandTest.java
@@ -1,17 +1,18 @@
package mock.model.commandFactory;
+import mock.exceptions.CommandConstructionException;
import mock.model.MockBoat;
import mock.model.MockRace;
+import mock.model.MockRaceTest;
+import network.Messages.BoatAction;
import network.Messages.Enums.BoatActionEnum;
import org.junit.Before;
import org.junit.Test;
-import org.mockito.Mock;
+import shared.exceptions.InvalidBoatDataException;
+import shared.exceptions.InvalidRaceDataException;
+import shared.exceptions.InvalidRegattaDataException;
import shared.model.Bearing;
-import shared.model.Boat;
-import shared.model.Race;
-import visualiser.model.VisualiserRace;
-import static org.mockito.Mockito.when;
import static org.testng.Assert.*;
import static org.mockito.Mockito.mock;
@@ -28,15 +29,23 @@ public class WindCommandTest {
private double offset = 3.0;
@Before
- public void setUp() {
- race = mock(MockRace.class);
- boat = new MockBoat(0, "Bob", "NZ", null);
+ public void setUp() throws CommandConstructionException, InvalidBoatDataException, InvalidRegattaDataException, InvalidRaceDataException {
+ race = MockRaceTest.createMockRace();
- when(race.getWindDirection()).thenReturn(Bearing.fromDegrees(0.0));
+ boat = race.getBoats().get(0);
+
+
+ //when(race.getWindDirection()).thenReturn(Bearing.fromDegrees(0.0));
boat.setBearing(Bearing.fromDegrees(45.0));
- upwind = CommandFactory.createCommand(race, boat, BoatActionEnum.UPWIND);
- downwind = CommandFactory.createCommand(race, boat, BoatActionEnum.DOWNWIND);
+ BoatAction upwindAction = new BoatAction(BoatActionEnum.UPWIND);
+ upwindAction.setSourceID(boat.getSourceID());
+
+ BoatAction downwindAction = new BoatAction(BoatActionEnum.DOWNWIND);
+ downwindAction.setSourceID(boat.getSourceID());
+
+ upwind = CommandFactory.createCommand(race, upwindAction);
+ downwind = CommandFactory.createCommand(race, downwindAction);
initial = boat.getBearing().degrees();
}
@@ -55,4 +64,4 @@ public class WindCommandTest {
downwind.execute();
assertEquals(initial - boat.getBearing().degrees(), offset, 1e-5);
}
-}
\ No newline at end of file
+}
diff --git a/racevisionGame/src/test/java/network/BinaryMessageDecoderTest.java b/racevisionGame/src/test/java/network/BinaryMessageDecoderTest.java
index 7414d709..46441775 100644
--- a/racevisionGame/src/test/java/network/BinaryMessageDecoderTest.java
+++ b/racevisionGame/src/test/java/network/BinaryMessageDecoderTest.java
@@ -2,6 +2,7 @@ package network;
import network.Exceptions.InvalidMessageException;
import network.MessageDecoders.XMLMessageDecoder;
+import network.MessageDecoders.XMLMessageDecoderTest;
import network.MessageEncoders.RaceVisionByteEncoder;
import network.Messages.AC35Data;
import network.Messages.Enums.MessageType;
@@ -21,29 +22,47 @@ import java.nio.charset.StandardCharsets;
import static org.junit.Assert.fail;
/**
- * Created by hba56 on 21/04/17.
+ * Tests the binary message decoder and encoder for a variety of messages.
*/
public class BinaryMessageDecoderTest {
+
+
+ /**
+ * Tests if an XMLMessage can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
@Test
- public void decodeTest(){
- try{
- String xmlString = XMLReader.readXMLFileToString("network/raceXML/Regatta.xml", StandardCharsets.UTF_8);
+ public void xmlMessageTest() throws Exception {
+
+ try {
+
+ String filePath = "network/raceXML/Regatta.xml";
+ XMLMessageType messageType = XMLMessageType.REGATTA;
+
+ String xmlString = XMLReader.readXMLFileToString(filePath, StandardCharsets.UTF_8);
long time = System.currentTimeMillis();
- XMLMessage xmlMessagePre = new XMLMessage(
+ XMLMessage xmlMessage = new XMLMessage(
(byte)1,
1,
time,
- XMLMessageType.REGATTA,
+ messageType,
(short)1,
- xmlString );
+ xmlString );
+
+ byte[] encodedMessage = RaceVisionByteEncoder.encode(xmlMessage);
- byte[] encodedMessage = RaceVisionByteEncoder.xmlMessage(xmlMessagePre);
- BinaryMessageEncoder testMessage = new BinaryMessageEncoder(MessageType.XMLMESSAGE, time, 1, (short)encodedMessage.length, encodedMessage);
+ BinaryMessageEncoder encoder = new BinaryMessageEncoder(
+ xmlMessage.getType(),
+ time,
+ 1,
+ (short)encodedMessage.length,
+ encodedMessage );
+
+ BinaryMessageDecoder decoder = new BinaryMessageDecoder(encoder.getFullMessage());
- BinaryMessageDecoder decoder = new BinaryMessageDecoder(testMessage.getFullMessage());
AC35Data message = null;
try {
@@ -56,7 +75,7 @@ public class BinaryMessageDecoderTest {
if (!(message instanceof XMLMessage)){
Assert.assertFalse(true);
}
- XMLMessage xmlMessage = (XMLMessage) message;
+ XMLMessage xmlMessageDecoded = (XMLMessage) message;
//message length
@@ -66,29 +85,17 @@ public class BinaryMessageDecoderTest {
//source ID
Assert.assertEquals((short) 1, decoder.getHeaderSourceID());
//message type
- Assert.assertEquals(26, decoder.getHeaderMessageType());
-
- XMLMessageDecoder decoderXML = new XMLMessageDecoder(decoder.getMessageBody());
- decoderXML.decode();
-
- //tests from seng302.Networking.MessageDecoders.XMLMessageDecoderTest to make sure the file is still good
- Assert.assertEquals((byte)1, decoderXML.getMessageVersionNumber());
- Assert.assertEquals((short)1, decoderXML.getAckNumber());
- Assert.assertEquals(time, decoderXML.getTimeStamp());
- Assert.assertEquals(XMLMessageType.REGATTA, decoderXML.getXmlMsgSubType());
- Assert.assertEquals((short)1, decoderXML.getSequenceNumber());
- Assert.assertEquals((short)xmlString.length(), decoderXML.getXmlMsgLength());
-
-// Reader reader = decoderXML.getXmlMessageInputStream().getCharacterStream();
-// int c;
-// String contents = "";
-// while((c = reader.read()) != -1) {
-// contents += (char)c;
-// }
-// Assert.assertEquals(xmlString.toString(), contents);
-
- } catch (XMLReaderException | TransformerException e){
+ Assert.assertEquals(MessageType.XMLMESSAGE.getValue(), decoder.getHeaderMessageType());
+
+
+ XMLMessageDecoderTest.compareXMLMessages(xmlMessage, xmlMessageDecoded);
+
+
+ } catch (XMLReaderException e){
fail("couldn't read file" + e.getMessage());
}
}
+
+ //TODO add some tests for more messages types.
+
}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/AverageWindDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/AverageWindDecoderTest.java
new file mode 100644
index 00000000..001c54eb
--- /dev/null
+++ b/racevisionGame/src/test/java/network/MessageDecoders/AverageWindDecoderTest.java
@@ -0,0 +1,75 @@
+package network.MessageDecoders;
+
+import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Messages.AverageWind;
+import static org.junit.Assert.*;
+import org.junit.Test;
+import shared.model.Bearing;
+
+import java.util.ArrayList;
+
+/**
+ * Test for the AverageWind encoder and decoder
+ */
+public class AverageWindDecoderTest {
+
+
+ /**
+ * Creates a AverageWind message, encodes it, decodes it, and checks that the result matches the starting message.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void averageWindEncodeDecodeTest() throws Exception {
+
+ AverageWind averageWind = new AverageWind(
+ AverageWind.currentMessageVersionNumber,
+ System.currentTimeMillis(),
+ 3000,
+ 12.5,
+ 4050,
+ 12.6,
+ 3055,
+ 12.7,
+ 6051,
+ 13.37
+ );
+
+ byte[] encodedMessage = RaceVisionByteEncoder.encode(averageWind);
+
+ AverageWindDecoder averageWindDecoder = new AverageWindDecoder();
+ averageWindDecoder.decode(encodedMessage);
+ AverageWind averageWindDecoded = averageWindDecoder.getMessage();
+
+ compareAverageWindMessages(averageWind, averageWindDecoded);
+
+ }
+
+
+ /**
+ * Compares two AverageWind messages to check that they are equal.
+ * @param original The original AverageWind message.
+ * @param decoded The decoded AverageWind message.
+ */
+ public static void compareAverageWindMessages(AverageWind original, AverageWind decoded) {
+
+
+ assertEquals(original.getMessageVersionNumber(), decoded.getMessageVersionNumber());
+ assertEquals(original.getTime(), decoded.getTime());
+
+ assertEquals(original.getRawPeriod(), decoded.getRawPeriod(), 100);
+ assertEquals(original.getRawSpeedKnots(), decoded.getRawSpeedKnots(), 0.01);
+
+ assertEquals(original.getSampleTwoPeriod(), decoded.getSampleTwoPeriod(), 100);
+ assertEquals(original.getSampleTwoSpeedKnots(), decoded.getSampleTwoSpeedKnots(), 0.01);
+
+ assertEquals(original.getSampleThreePeriod(), decoded.getSampleThreePeriod(), 100);
+ assertEquals(original.getSampleThreeSpeedKnots(), decoded.getSampleThreeSpeedKnots(), 0.01);
+
+ assertEquals(original.getSampleFourPeriod(), decoded.getSampleFourPeriod(), 100);
+ assertEquals(original.getSampleFourSpeedKnots(), decoded.getSampleFourSpeedKnots(), 0.01);
+
+
+ }
+
+
+}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/BoatActionDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/BoatActionDecoderTest.java
new file mode 100644
index 00000000..80dacbe6
--- /dev/null
+++ b/racevisionGame/src/test/java/network/MessageDecoders/BoatActionDecoderTest.java
@@ -0,0 +1,117 @@
+package network.MessageDecoders;
+
+import network.Exceptions.InvalidMessageException;
+import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Messages.BoatAction;
+import network.Messages.Enums.BoatActionEnum;
+import network.Messages.Enums.RequestToJoinEnum;
+import network.Messages.RequestToJoin;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+
+/**
+ * Test for the BoatAction encoder and decoder
+ */
+public class BoatActionDecoderTest {
+
+
+ /**
+ * Encodes and decodes a given message.
+ * @param message Message to encode/decode.
+ * @return The decoded message.
+ * @throws InvalidMessageException If the message cannot be encoded.
+ */
+ private BoatAction encodeDecodeMessage(BoatAction message) throws InvalidMessageException {
+
+ //Encode.
+ byte [] testEncodedMessage = RaceVisionByteEncoder.encode(message);
+
+ //Decode.
+ BoatActionDecoder testDecoder = new BoatActionDecoder();
+ testDecoder.decode(testEncodedMessage);
+
+ BoatAction decodedMessage = testDecoder.getMessage();
+
+ return decodedMessage;
+ }
+
+
+ /**
+ * Tests if a specific boat action type message can be encoded and decoded correctly.
+ * @param type The type of boat action.
+ * @throws Exception if test fails.
+ */
+ private void boatActionTypeTest(BoatActionEnum type) throws Exception {
+
+ //Prepare message.
+ BoatAction beforeMessage = new BoatAction(type);
+
+
+ //Encode/decode it.
+ BoatAction afterMessage = encodeDecodeMessage(beforeMessage);
+
+
+ //Compare.
+ assertEquals(beforeMessage.getBoatAction(), afterMessage.getBoatAction());
+
+ }
+
+
+ /**
+ * Tests if an autopilot message can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void autoPilotTest() throws Exception {
+ boatActionTypeTest(BoatActionEnum.AUTO_PILOT);
+ }
+
+ /**
+ * Tests if a sails in message can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void sailsInTest() throws Exception {
+ boatActionTypeTest(BoatActionEnum.SAILS_IN);
+ }
+
+ /**
+ * Tests if a sails out message can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void sailsOutTest() throws Exception {
+ boatActionTypeTest(BoatActionEnum.SAILS_OUT);
+ }
+
+ /**
+ * Tests if a tack/gybe message can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void tackGybeTest() throws Exception {
+ boatActionTypeTest(BoatActionEnum.TACK_GYBE);
+ }
+
+ /**
+ * Tests if an upwind message can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void upwindTest() throws Exception {
+ boatActionTypeTest(BoatActionEnum.UPWIND);
+ }
+
+ /**
+ * Tests if a downwind message can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void downwindTest() throws Exception {
+ boatActionTypeTest(BoatActionEnum.DOWNWIND);
+ }
+
+
+}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/BoatLocationDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/BoatLocationDecoderTest.java
index 2f269257..7fcdeb4a 100644
--- a/racevisionGame/src/test/java/network/MessageDecoders/BoatLocationDecoderTest.java
+++ b/racevisionGame/src/test/java/network/MessageDecoders/BoatLocationDecoderTest.java
@@ -2,48 +2,82 @@ package network.MessageDecoders;
import network.MessageEncoders.RaceVisionByteEncoder;
import network.Messages.BoatLocation;
+import network.Messages.Enums.BoatLocationDeviceEnum;
import org.junit.Assert;
import org.junit.Test;
+import shared.model.Azimuth;
+import shared.model.Bearing;
/**
- * Created by hba56 on 23/04/17.
+ * Test for the BoatLocation encoder and decoder
*/
public class BoatLocationDecoderTest {
+
+
+ /**
+ * Creates a BoatLocation message, encodes it, decodes it, and checks that the result matches the starting message.
+ * @throws Exception if test fails.
+ */
@Test
- public void getByteArrayTest(){
+ public void boatLocationEncodeDecodeTest() throws Exception {
+
+ //Create message.
long time = System.currentTimeMillis();
- BoatLocation testMessage = new BoatLocation((byte) 1, time, 2,
- 3, (byte) 1, 180, -180, 4, 5,
- (short) 6, (short) 7, 8, 9, 10, 11,
- (short) 12, 13, 14 , (short) 15,
- 16, 17, (short) 18);
- byte [] testEncodedMessage = RaceVisionByteEncoder.boatLocation(testMessage);
+ BoatLocation testMessage = new BoatLocation(
+ BoatLocation.currentMessageVersionNumber,
+ time,
+ 2,
+ 3,
+ BoatLocationDeviceEnum.RacingYacht,
+ 180,
+ -180,
+ 4,
+ Bearing.fromDegrees(45),
+ (short) 6,
+ (short) 7,
+ 8,
+ Bearing.fromDegrees(40),
+ 10,
+ 11,
+ Azimuth.fromDegrees(35),
+ 13,
+ Bearing.fromDegrees(80),
+ Azimuth.fromDegrees(80),
+ 16,
+ Bearing.fromDegrees(80),
+ Azimuth.fromDegrees(22) );
- BoatLocationDecoder testDecoder = new BoatLocationDecoder(testEncodedMessage);
+ //Encode.
+ byte [] testEncodedMessage = RaceVisionByteEncoder.encode(testMessage);
+
+ //Decode.
+ BoatLocationDecoder testDecoder = new BoatLocationDecoder();
+ testDecoder.decode(testEncodedMessage);
BoatLocation decodedTest = testDecoder.getMessage();
+ //Check if valid.
Assert.assertEquals(testMessage.getMessageVersionNumber(), decodedTest.getMessageVersionNumber());
Assert.assertEquals(testMessage.getTime(), decodedTest.getTime());
Assert.assertEquals(testMessage.getSequenceNumber(), decodedTest.getSequenceNumber());
Assert.assertEquals(testMessage.getDeviceType(), decodedTest.getDeviceType());
- Assert.assertEquals(testMessage.getLatitude(), decodedTest.getLatitude());
- Assert.assertEquals(testMessage.getLongitude(), decodedTest.getLongitude());
+ Assert.assertEquals(testMessage.getLatitude(), decodedTest.getLatitude(), 0.01);
+ Assert.assertEquals(testMessage.getLongitude(), decodedTest.getLongitude(), 0.01);
Assert.assertEquals(testMessage.getAltitude(), decodedTest.getAltitude());
- Assert.assertEquals(testMessage.getHeading(), decodedTest.getHeading());
+ Assert.assertEquals(testMessage.getHeading().degrees(), decodedTest.getHeading().degrees(), 0.01);
Assert.assertEquals(testMessage.getPitch(), decodedTest.getPitch());
Assert.assertEquals(testMessage.getRoll(), decodedTest.getRoll());
- Assert.assertEquals(testMessage.getBoatSpeed(), decodedTest.getBoatSpeed());
-
- Assert.assertEquals(testMessage.getBoatCOG(), decodedTest.getBoatCOG());
- Assert.assertEquals(testMessage.getBoatSOG(), decodedTest.getBoatSOG());
- Assert.assertEquals(testMessage.getApparentWindSpeed(), decodedTest.getApparentWindSpeed());
- Assert.assertEquals(testMessage.getTrueWindSpeed(), decodedTest.getTrueWindSpeed());
- Assert.assertEquals(testMessage.getTrueWindDirection(), decodedTest.getTrueWindDirection());
- Assert.assertEquals(testMessage.getTrueWindAngle(), decodedTest.getTrueWindAngle());
- Assert.assertEquals(testMessage.getCurrentDrift(), decodedTest.getCurrentDrift());
- Assert.assertEquals(testMessage.getCurrentSet(), decodedTest.getCurrentSet());
- Assert.assertEquals(testMessage.getRudderAngle(), decodedTest.getRudderAngle());
+ Assert.assertEquals(testMessage.getBoatSpeedKnots(), decodedTest.getBoatSpeedKnots(), 0.01);
+
+ Assert.assertEquals(testMessage.getBoatCOG().degrees(), decodedTest.getBoatCOG().degrees(), 0.01);
+ Assert.assertEquals(testMessage.getBoatSOGKnots(), decodedTest.getBoatSOGKnots(), 0.01);
+ Assert.assertEquals(testMessage.getApparentWindSpeedKnots(), decodedTest.getApparentWindSpeedKnots(), 0.01);
+ Assert.assertEquals(testMessage.getTrueWindSpeedKnots(), decodedTest.getTrueWindSpeedKnots(), 0.01);
+ Assert.assertEquals(testMessage.getTrueWindDirection().degrees(), decodedTest.getTrueWindDirection().degrees(), 0.01);
+ Assert.assertEquals(testMessage.getTrueWindAngle().degrees(), decodedTest.getTrueWindAngle().degrees(), 0.01);
+ Assert.assertEquals(testMessage.getCurrentDriftKnots(), decodedTest.getCurrentDriftKnots(), 0.01);
+ Assert.assertEquals(testMessage.getCurrentSet().degrees(), decodedTest.getCurrentSet().degrees(), 0.01);
+ Assert.assertEquals(testMessage.getRudderAngle().degrees(), decodedTest.getRudderAngle().degrees(), 0.01);
}
}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/BoatStatusDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/BoatStatusDecoderTest.java
new file mode 100644
index 00000000..45a2e1db
--- /dev/null
+++ b/racevisionGame/src/test/java/network/MessageDecoders/BoatStatusDecoderTest.java
@@ -0,0 +1,90 @@
+package network.MessageDecoders;
+
+import network.Exceptions.InvalidMessageException;
+import network.MessageEncoders.BoatStatusEncoder;
+import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Messages.BoatStatus;
+import network.Messages.Enums.BoatStatusEnum;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Test for the BoatStatus encoder and decoder
+ */
+public class BoatStatusDecoderTest {
+
+
+ /**
+ * Creates a BoatStatus message, encodes it, decodes it, and checks that the result matches the starting message.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void boatStatusEncodeDecodeTest() throws Exception {
+
+ long time = System.currentTimeMillis();
+
+ //Create data to serialize.
+ int boatSourceID = 5;
+ BoatStatusEnum boatStatusEnum = BoatStatusEnum.RACING;
+ byte boatLegNumber = 5;
+ byte boatPenaltiesAwarded = 4;
+ byte boatPenaltiesServed = 2;
+ long boatTimeAtNextMark = time + (1000 * 3);
+ long boatTimeAtFinish = boatTimeAtNextMark + (1000 * 15);
+
+ BoatStatus boatStatus = new BoatStatus(
+ boatSourceID,
+ boatStatusEnum,
+ boatLegNumber,
+ boatPenaltiesAwarded,
+ boatPenaltiesServed,
+ boatTimeAtNextMark,
+ boatTimeAtFinish );
+
+
+ BoatStatus boatStatusDecoded = encodeDecodeBoatStatus(boatStatus);
+
+ compareBoatStatusMessages(boatStatus, boatStatusDecoded);
+
+ }
+
+ /**
+ * Encodes and decodes a BoatStatus, and returns it.
+ * @param boatStatus The BoatStatus to encode and decode.
+ * @return The decoded BoatStatus.
+ * @throws InvalidMessageException Thrown if message cannot be encoded or decoded.
+ */
+ private static BoatStatus encodeDecodeBoatStatus(BoatStatus boatStatus) throws InvalidMessageException {
+
+ BoatStatusEncoder boatStatusEncoder = new BoatStatusEncoder();
+ byte[] boatStatusEncoded = boatStatusEncoder.encode(boatStatus);
+
+ BoatStatusDecoder boatStatusDecoder = new BoatStatusDecoder();
+ BoatStatus boatStatusDecoded = boatStatusDecoder.decode(boatStatusEncoded);
+
+ return boatStatusDecoded;
+ }
+
+
+ /**
+ * Compares two BoatStatus messages to check that they are equal.
+ * @param original The original BoatStatus message.
+ * @param decoded The decoded BoatStatus message.
+ */
+ public static void compareBoatStatusMessages(BoatStatus original, BoatStatus decoded) {
+
+ Assert.assertEquals(original.getSourceID(), decoded.getSourceID());
+ Assert.assertEquals(original.getBoatStatus(), decoded.getBoatStatus());
+ Assert.assertEquals(original.getLegNumber(), decoded.getLegNumber());
+ Assert.assertEquals(original.getNumPenaltiesAwarded(), decoded.getNumPenaltiesAwarded());
+ Assert.assertEquals(original.getNumPenaltiesServed(), decoded.getNumPenaltiesServed());
+ Assert.assertEquals(original.getEstTimeAtNextMark(), decoded.getEstTimeAtNextMark());
+ Assert.assertEquals(original.getEstTimeAtFinish(), decoded.getEstTimeAtFinish());
+
+ }
+
+}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/CourseWindDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/CourseWindDecoderTest.java
index 53793b81..cef452f2 100644
--- a/racevisionGame/src/test/java/network/MessageDecoders/CourseWindDecoderTest.java
+++ b/racevisionGame/src/test/java/network/MessageDecoders/CourseWindDecoderTest.java
@@ -1,57 +1,79 @@
package network.MessageDecoders;
-import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Exceptions.InvalidMessageException;
+import network.MessageEncoders.CourseWindEncoder;
+import network.Messages.BoatStatus;
import network.Messages.CourseWind;
+import network.Messages.Enums.BoatStatusEnum;
import org.junit.Assert;
import org.junit.Test;
-
-import java.util.ArrayList;
-
+import shared.model.Bearing;
/**
- * Created by hba56 on 23/04/17.
+ * Test for the CourseWind encoder and decoder
*/
public class CourseWindDecoderTest {
+
+
+ /**
+ * Creates a CourseWind message, encodes it, decodes it, and checks that the result matches the starting message.
+ * @throws Exception if test fails.
+ */
@Test
- public void getByteArrayTest(){
+ public void courseWindEncodeDecodeTest() throws Exception {
+
long time = System.currentTimeMillis();
- CourseWind testCourseWind1 = new CourseWind(1, time, 2,
- 3, 4, 5,
- 7, 6);
+ CourseWind courseWind = new CourseWind(
+ 1,
+ time,
+ 2,
+ Bearing.fromDegrees(45),
+ 4,
+ Bearing.fromDegrees(70),
+ Bearing.fromDegrees(160),
+ (byte) 0x13 );
- long time2 = System.currentTimeMillis();
- CourseWind testCourseWind2 = new CourseWind(2, time2, 2,
- 3, 4, 5,
- 7, 6);
- ArrayList testCourseWinds = new ArrayList();
- testCourseWinds.add(testCourseWind1);
- testCourseWinds.add(testCourseWind2);
+ CourseWind courseWindDecoded = encodeDecodeCourseWind(courseWind);
+ compareCourseWindMessages(courseWind, courseWindDecoded);
- byte[] testEncodedCourseWind = RaceVisionByteEncoder.courseWind((byte) 1, testCourseWinds);
+ }
- CourseWindDecoder testDecoder = new CourseWindDecoder(testEncodedCourseWind);
+ /**
+ * Encodes and decodes a CourseWind, and returns it.
+ * @param courseWind The CourseWind to encode and decode.
+ * @return The decoded CourseWind.
+ * @throws InvalidMessageException Thrown if message cannot be encoded or decoded.
+ */
+ private static CourseWind encodeDecodeCourseWind(CourseWind courseWind) throws InvalidMessageException {
- ArrayList testDecodedCourseWinds = testDecoder.getLoopMessages();
+ CourseWindEncoder courseWindEncoder = new CourseWindEncoder();
+ byte[] courseWindEncoded = courseWindEncoder.encode(courseWind);
- Assert.assertEquals(testCourseWinds.get(0).getID(), testDecodedCourseWinds.get(0).getID());
- Assert.assertEquals(testCourseWinds.get(0).getTime(), testDecodedCourseWinds.get(0).getTime());
- Assert.assertEquals(testCourseWinds.get(0).getRaceID(), testDecodedCourseWinds.get(0).getRaceID());
- Assert.assertEquals(testCourseWinds.get(0).getWindDirection(), testDecodedCourseWinds.get(0).getWindDirection());
- Assert.assertEquals(testCourseWinds.get(0).getWindSpeed(), testDecodedCourseWinds.get(0).getWindSpeed());
- Assert.assertEquals(testCourseWinds.get(0).getBestUpwindAngle(), testDecodedCourseWinds.get(0).getBestUpwindAngle());
- Assert.assertEquals(testCourseWinds.get(0).getBestDownwindAngle(), testDecodedCourseWinds.get(0).getBestDownwindAngle());
- Assert.assertEquals(testCourseWinds.get(0).getFlags(), testDecodedCourseWinds.get(0).getFlags());
+ CourseWindDecoder courseWindDecoder = new CourseWindDecoder();
+ CourseWind courseWindDecoded = courseWindDecoder.decode(courseWindEncoded);
- Assert.assertEquals(testCourseWinds.get(1).getID(), testDecodedCourseWinds.get(1).getID());
- Assert.assertEquals(testCourseWinds.get(1).getTime(), testDecodedCourseWinds.get(1).getTime());
- Assert.assertEquals(testCourseWinds.get(1).getRaceID(), testDecodedCourseWinds.get(1).getRaceID());
- Assert.assertEquals(testCourseWinds.get(1).getWindDirection(), testDecodedCourseWinds.get(1).getWindDirection());
- Assert.assertEquals(testCourseWinds.get(1).getWindSpeed(), testDecodedCourseWinds.get(1).getWindSpeed());
- Assert.assertEquals(testCourseWinds.get(1).getBestUpwindAngle(), testDecodedCourseWinds.get(1).getBestUpwindAngle());
- Assert.assertEquals(testCourseWinds.get(1).getBestDownwindAngle(), testDecodedCourseWinds.get(1).getBestDownwindAngle());
- Assert.assertEquals(testCourseWinds.get(1).getFlags(), testDecodedCourseWinds.get(1).getFlags());
+ return courseWindDecoded;
+ }
+
+
+ /**
+ * Compares two CourseWind messages to check that they are equal.
+ * @param original The original CourseWind message.
+ * @param decoded The decoded CourseWind message.
+ */
+ public static void compareCourseWindMessages(CourseWind original, CourseWind decoded) {
+
+ Assert.assertEquals(original.getID(), decoded.getID());
+ Assert.assertEquals(original.getTime(), decoded.getTime());
+ Assert.assertEquals(original.getRaceID(), decoded.getRaceID());
+ Assert.assertEquals(original.getWindDirection().degrees(), decoded.getWindDirection().degrees(), 0.01);
+ Assert.assertEquals(original.getWindSpeedKnots(), decoded.getWindSpeedKnots(), 0.01);
+ Assert.assertEquals(original.getBestUpwindAngle().degrees(), decoded.getBestUpwindAngle().degrees(), 0.01);
+ Assert.assertEquals(original.getBestDownwindAngle().degrees(), decoded.getBestDownwindAngle().degrees(), 0.01);
+ Assert.assertEquals(original.getFlags(), decoded.getFlags());
}
+
}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/CourseWindsDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/CourseWindsDecoderTest.java
new file mode 100644
index 00000000..45c87f55
--- /dev/null
+++ b/racevisionGame/src/test/java/network/MessageDecoders/CourseWindsDecoderTest.java
@@ -0,0 +1,96 @@
+package network.MessageDecoders;
+
+import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Messages.CourseWind;
+import network.Messages.CourseWinds;
+import org.junit.Test;
+import shared.model.Bearing;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests for CourseWinds encoder and decoder.
+ */
+public class CourseWindsDecoderTest {
+
+ /**
+ * Tests if a CourseWinds message can be encoded and decoded correctly.
+ * @throws Exception Thrown if an error occurs.
+ */
+ @Test
+ public void courseWindsEncodeDecodeTest() throws Exception {
+
+ long time1 = System.currentTimeMillis();
+ CourseWind testCourseWind1 = new CourseWind(
+ 1,
+ time1,
+ 2,
+ Bearing.fromDegrees(45),
+ 4,
+ Bearing.fromDegrees(70),
+ Bearing.fromDegrees(160),
+ (byte) 0xCE );
+
+ long time2 = System.currentTimeMillis();
+ CourseWind testCourseWind2 = new CourseWind(
+ 2,
+ time2,
+ 2,
+ Bearing.fromDegrees(55),
+ 4,
+ Bearing.fromDegrees(80),
+ Bearing.fromDegrees(180),
+ (byte) 0x0D );
+
+ List testCourseWinds = new ArrayList<>();
+ testCourseWinds.add(testCourseWind1);
+ testCourseWinds.add(testCourseWind2);
+
+ CourseWinds courseWinds = new CourseWinds(CourseWinds.currentMessageVersionNumber, (byte) 2, testCourseWinds);
+
+
+ byte[] testEncodedCourseWind = RaceVisionByteEncoder.encode(courseWinds);
+
+ CourseWindsDecoder courseWindsDecoder = new CourseWindsDecoder();
+ courseWindsDecoder.decode(testEncodedCourseWind);
+ CourseWinds courseWindsDecoded = courseWindsDecoder.getMessage();
+
+ compareCourseWindsMessages(courseWinds, courseWindsDecoded);
+
+
+ }
+
+
+ /**
+ * Compares two course winds messages to ensure they are the same.
+ * @param original The original message.
+ * @param decoded The decoded message.
+ */
+ public static void compareCourseWindsMessages(CourseWinds original, CourseWinds decoded) {
+
+ //Compare header.
+ assertEquals(original.getMessageVersionNumber(), decoded.getMessageVersionNumber());
+ assertEquals(original.getSelectedWindID(), decoded.getSelectedWindID());
+ assertEquals(original.getCourseWinds().size(), decoded.getCourseWinds().size());
+
+ //Compare each CourseWind.
+ List originalWinds = original.getCourseWinds();
+ List decodedWinds = decoded.getCourseWinds();
+
+ Iterator originalIterator = originalWinds.iterator();
+ Iterator decodedIterator = decodedWinds.iterator();
+
+ while (originalIterator.hasNext() && decodedIterator.hasNext()) {
+
+ CourseWindDecoderTest.compareCourseWindMessages(originalIterator.next(), decodedIterator.next());
+
+ }
+
+ }
+}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/HeartBeatDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/HeartBeatDecoderTest.java
new file mode 100644
index 00000000..ea1de885
--- /dev/null
+++ b/racevisionGame/src/test/java/network/MessageDecoders/HeartBeatDecoderTest.java
@@ -0,0 +1,79 @@
+package network.MessageDecoders;
+
+import network.Exceptions.InvalidMessageException;
+import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Messages.BoatAction;
+import network.Messages.Enums.BoatActionEnum;
+import network.Messages.HeartBeat;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+
+/**
+ * Test for the HeartBeat encoder and decoder
+ */
+public class HeartBeatDecoderTest {
+
+
+ /**
+ * Encodes and decodes a given message.
+ * @param message Message to encode/decode.
+ * @return The decoded message.
+ * @throws InvalidMessageException if the message cannot be encoded.
+ */
+ private HeartBeat encodeDecodeMessage(HeartBeat message) throws InvalidMessageException {
+
+ //Encode.
+ byte [] testEncodedMessage = RaceVisionByteEncoder.encode(message);
+
+ //Decode.
+ HeartBeatDecoder testDecoder = new HeartBeatDecoder();
+ testDecoder.decode(testEncodedMessage);
+ HeartBeat decodedMessage = testDecoder.getMessage();
+
+ return decodedMessage;
+ }
+
+
+ /**
+ * Tests if a heartbeat message with a given sequence number can be encoded and decoded correctly.
+ * @param sequenceNumber The sequenceNumber to use.
+ * @throws Exception if test fails.
+ */
+ private void heartBeatTest(long sequenceNumber) throws Exception {
+
+ //Prepare message.
+ HeartBeat beforeMessage = new HeartBeat(sequenceNumber);
+
+
+ //Encode/decode it.
+ HeartBeat afterMessage = encodeDecodeMessage(beforeMessage);
+
+
+ //Compare.
+ assertEquals(beforeMessage.getSequenceNumber(), afterMessage.getSequenceNumber());
+
+ }
+
+
+ /**
+ * Tests if a heartbeat message with a sequence number of zero can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void heartBeatZeroTest() throws Exception {
+ heartBeatTest(0);
+ }
+
+ /**
+ * Tests if a heartbeat message with a sequence number of 1234512 can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void heartBeatNonZeroTest() throws Exception {
+ heartBeatTest(1234512);
+ }
+
+
+}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/JoinAcceptanceDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/JoinAcceptanceDecoderTest.java
new file mode 100644
index 00000000..6c287dd6
--- /dev/null
+++ b/racevisionGame/src/test/java/network/MessageDecoders/JoinAcceptanceDecoderTest.java
@@ -0,0 +1,91 @@
+package network.MessageDecoders;
+
+import network.Exceptions.InvalidMessageException;
+import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Messages.Enums.JoinAcceptanceEnum;
+import network.Messages.JoinAcceptance;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Test for the {@link network.Messages.JoinAcceptance} encoder and decoder
+ */
+public class JoinAcceptanceDecoderTest {
+
+
+ /**
+ * Encodes and decodes a given message.
+ * @param message Message to encode/decode.
+ * @return The decoded message.
+ * @throws InvalidMessageException If the message cannot be encoded.
+ */
+ private JoinAcceptance encodeDecodeMessage(JoinAcceptance message) throws InvalidMessageException {
+
+ //Encode.
+ byte [] testEncodedMessage = RaceVisionByteEncoder.encode(message);
+
+ //Decode.
+ JoinAcceptanceDecoder testDecoder = new JoinAcceptanceDecoder();
+ testDecoder.decode(testEncodedMessage);
+ JoinAcceptance decodedMessage = testDecoder.getMessage();
+
+ return decodedMessage;
+ }
+
+
+ /**
+ * Tests if a specific acceptance type message can be encoded and decoded correctly.
+ * @param type The type of acceptance response.
+ * @param sourceID The source ID to assign.
+ * @throws Exception if test fails.
+ */
+ private void responseTypeTest(JoinAcceptanceEnum type, int sourceID) throws Exception {
+
+ //Prepare message.
+ JoinAcceptance beforeMessage = new JoinAcceptance(type, sourceID);
+
+
+ //Encode/decode it.
+ JoinAcceptance afterMessage = encodeDecodeMessage(beforeMessage);
+
+
+ //Compare.
+ assertEquals(beforeMessage.getAcceptanceType().getValue(), afterMessage.getAcceptanceType().getValue());
+ assertEquals(beforeMessage.getSourceID(), afterMessage.getSourceID());
+
+ }
+
+
+ /**
+ * Tests if a join success message, with a source ID, can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void joinSuccessSourceIDTest() throws Exception {
+ responseTypeTest(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, 12345);
+ }
+
+ /**
+ * Tests if a join success message, with no source ID, can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void joinSuccessNoSourceIDTest() throws Exception {
+ responseTypeTest(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, 0);
+ }
+
+
+ /**
+ * Tests if a server full message can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void serverFullTest() throws Exception {
+ responseTypeTest(JoinAcceptanceEnum.SERVER_FULL, 0);
+ }
+
+
+
+}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/MarkRoundingDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/MarkRoundingDecoderTest.java
new file mode 100644
index 00000000..3fce2176
--- /dev/null
+++ b/racevisionGame/src/test/java/network/MessageDecoders/MarkRoundingDecoderTest.java
@@ -0,0 +1,69 @@
+package network.MessageDecoders;
+
+import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Messages.Enums.MarkRoundingBoatStatusEnum;
+import network.Messages.Enums.MarkRoundingSideEnum;
+import network.Messages.Enums.MarkRoundingTypeEnum;
+import network.Messages.MarkRounding;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test for the MarkRounding encoder and decoder
+ */
+public class MarkRoundingDecoderTest {
+
+
+ /**
+ * Creates a MarkRounding message, encodes it, decodes it, and checks that the result matches the starting message.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void markRoundingEncodeDecodeTest() throws Exception {
+
+ MarkRounding markRounding = new MarkRounding(
+ MarkRounding.currentMessageVersionNumber,
+ System.currentTimeMillis(),
+ 567,
+ 42,
+ 125,
+ MarkRoundingBoatStatusEnum.RACING,
+ MarkRoundingSideEnum.PORT,
+ MarkRoundingTypeEnum.MARK,
+ (byte)45
+ );
+
+ byte[] encodedMessage = RaceVisionByteEncoder.encode(markRounding);
+
+ MarkRoundingDecoder markRoundingDecoder = new MarkRoundingDecoder();
+ markRoundingDecoder.decode(encodedMessage);
+ MarkRounding markRoundingDecoded = markRoundingDecoder.getMessage();
+
+ compareMarkRoundingMessages(markRounding, markRoundingDecoded);
+
+ }
+
+
+ /**
+ * Compares two MarkRounding messages to check that they are equal.
+ * @param original The original MarkRounding message.
+ * @param decoded The decoded MarkRounding message.
+ */
+ public static void compareMarkRoundingMessages(MarkRounding original, MarkRounding decoded) {
+
+
+ assertEquals(original.getMessageVersionNumber(), decoded.getMessageVersionNumber());
+ assertEquals(original.getTime(), decoded.getTime());
+ assertEquals(original.getAckNum(), decoded.getAckNum());
+ assertEquals(original.getRaceID(), decoded.getRaceID());
+ assertEquals(original.getSourceID(), decoded.getSourceID());
+ assertEquals(original.getBoatStatus(), decoded.getBoatStatus());
+ assertEquals(original.getRoundingSide(), decoded.getRoundingSide());
+ assertEquals(original.getMarkType(), decoded.getMarkType());
+ assertEquals(original.getMarkID(), decoded.getMarkID());
+
+ }
+
+
+}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/RaceStartStatusDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/RaceStartStatusDecoderTest.java
index ce2fe475..b0029321 100644
--- a/racevisionGame/src/test/java/network/MessageDecoders/RaceStartStatusDecoderTest.java
+++ b/racevisionGame/src/test/java/network/MessageDecoders/RaceStartStatusDecoderTest.java
@@ -1,29 +1,62 @@
package network.MessageDecoders;
import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Messages.Enums.RaceStartTypeEnum;
+import network.Messages.RaceStartStatus;
import org.junit.Assert;
import org.junit.Test;
/**
- * Created by hba56 on 23/04/17.
+ * Tests for the RaceStartStatus encoder and decoder.
*/
public class RaceStartStatusDecoderTest {
+
+
+ /**
+ * Tests if a RaceStartStatus message can be encoded and decoded correctly.
+ * @throws Exception Thrown when an error occurs.
+ */
@Test
- public void getByteArrayTest(){
- long time = System.currentTimeMillis();
+ public void raceStartStatusEncodeDecodeTest() throws Exception {
+
+ long timestamp = System.currentTimeMillis();
- long time2 = System.currentTimeMillis();
- byte[] encodedRaceStartStatus = RaceVisionByteEncoder.raceStartStatus(time, (short)1,
- time2, 2, (char)3);
+ long startTime = System.currentTimeMillis() + 10 * 1000;
- RaceStartStatusDecoder testDecoder = new RaceStartStatusDecoder(encodedRaceStartStatus);
+ RaceStartStatus raceStartStatus = new RaceStartStatus(
+ RaceStartStatus.currentMessageVersionNumber,
+ timestamp,
+ 55,
+ startTime,
+ 35,
+ RaceStartTypeEnum.SET_RACE_START
+ );
- Assert.assertEquals(0b1, testDecoder.getMessageVersion());
- Assert.assertEquals(time, testDecoder.getTime());
- Assert.assertEquals(1, testDecoder.getAck());
- Assert.assertEquals(time2, testDecoder.getStartTime());
- Assert.assertEquals(2, testDecoder.getRaceID());
- Assert.assertEquals((char)3, testDecoder.getNotification());
+ byte[] encodedRaceStartStatus = RaceVisionByteEncoder.encode(raceStartStatus);
+
+ RaceStartStatusDecoder testDecoder = new RaceStartStatusDecoder();
+ testDecoder.decode(encodedRaceStartStatus);
+ RaceStartStatus raceStartStatusDecoded = testDecoder.getMessage();
+
+ compareRaceStartStatusMessages(raceStartStatus, raceStartStatusDecoded);
}
+
+ /**
+ * Compares two RaceStartStatus messages to check that they are the same.
+ * @param original The original message.
+ * @param decoded The decoded message.
+ */
+ public static void compareRaceStartStatusMessages(RaceStartStatus original, RaceStartStatus decoded) {
+
+ Assert.assertEquals(original.getMessageVersionNumber(), decoded.getMessageVersionNumber());
+ Assert.assertEquals(original.getTimestamp(), decoded.getTimestamp());
+ Assert.assertEquals(original.getAckNum(), decoded.getAckNum());
+ Assert.assertEquals(original.getRaceStartTime(), decoded.getRaceStartTime());
+ Assert.assertEquals(original.getRaceID(), decoded.getRaceID());
+ Assert.assertEquals(original.getNotificationType(), decoded.getNotificationType());
+
+ }
+
+
}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/RaceStatusDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/RaceStatusDecoderTest.java
index a5600446..d9a47b81 100644
--- a/racevisionGame/src/test/java/network/MessageDecoders/RaceStatusDecoderTest.java
+++ b/racevisionGame/src/test/java/network/MessageDecoders/RaceStatusDecoderTest.java
@@ -1,27 +1,40 @@
package network.MessageDecoders;
+import network.Exceptions.InvalidMessageException;
import network.MessageEncoders.RaceVisionByteEncoder;
import network.Messages.BoatStatus;
+import network.Messages.Enums.BoatStatusEnum;
+import network.Messages.Enums.RaceStatusEnum;
+import network.Messages.Enums.RaceTypeEnum;
import network.Messages.RaceStatus;
import org.junit.Assert;
import org.junit.Test;
+import shared.model.Bearing;
import java.util.ArrayList;
+import java.util.Iterator;
import java.util.List;
/**
- * Created by hba56 on 23/04/17.
+ * Test for the RaceStatus encoder and decoder
*/
public class RaceStatusDecoderTest {
+
+
+ /**
+ * Creates a RaceStatus message, encodes it, decodes it, and checks that the result matches the starting message.
+ * @throws Exception if test fails.
+ */
@Test
- public void getByteArrayTest(){
+ public void raceStatusEncodeDecodeTest() throws Exception {
+
long time = System.currentTimeMillis();
//Create data to serialize.
int boat1SourceID = 5;
int boat2SourceID = 8;
- byte boat1Status = 2;
- byte boat2Status = 2;
+ BoatStatusEnum boat1Status = BoatStatusEnum.RACING;
+ BoatStatusEnum boat2Status = BoatStatusEnum.RACING;
byte boat1LegNumber = 5;
byte boat2LegNumber = 3;
byte boat1PenaltiesAwarded = 4;
@@ -33,44 +46,86 @@ public class RaceStatusDecoderTest {
long boat1TimeAtFinish = boat1TimeAtNextMark + (1000 * 15);
long boat2TimeAtFinish = boat2TimeAtNextMark + (1000 * 7);
- BoatStatus boatStatus1 = new BoatStatus(boat1SourceID, boat1Status, boat1LegNumber, boat1PenaltiesAwarded, boat1PenaltiesServed, boat1TimeAtNextMark, boat1TimeAtFinish);
- BoatStatus boatStatus2 = new BoatStatus(boat2SourceID, boat2Status, boat2LegNumber, boat2PenaltiesAwarded, boat2PenaltiesServed, boat2TimeAtNextMark, boat2TimeAtFinish);
+ BoatStatus boatStatus1 = new BoatStatus(
+ boat1SourceID,
+ boat1Status,
+ boat1LegNumber,
+ boat1PenaltiesAwarded,
+ boat1PenaltiesServed,
+ boat1TimeAtNextMark,
+ boat1TimeAtFinish );
+
+ BoatStatus boatStatus2 = new BoatStatus(
+ boat2SourceID,
+ boat2Status,
+ boat2LegNumber,
+ boat2PenaltiesAwarded,
+ boat2PenaltiesServed,
+ boat2TimeAtNextMark,
+ boat2TimeAtFinish );
+
int raceID = 585;
- byte raceStatus = 3;
+ RaceStatusEnum raceStatus = RaceStatusEnum.STARTED;
long raceStartTime = time - (1000 * 31);
- int windDirection = 2341;
- int windSpeed = 10201;
- int raceType = 1;
+ Bearing windDirection = Bearing.fromDegrees(185.34);
+ double windSpeedKnots = 14.52;
+ RaceTypeEnum raceType = RaceTypeEnum.MATCH_RACE;
List boatStatuses = new ArrayList<>(2);
boatStatuses.add(boatStatus1);
boatStatuses.add(boatStatus2);
- RaceStatus raceStatusObject = new RaceStatus(time, raceID, raceStatus, raceStartTime, windDirection, windSpeed, raceType, boatStatuses);
+ RaceStatus raceStatusOriginal = new RaceStatus(
+ RaceStatus.currentMessageVersionNumber,
+ time,
+ raceID,
+ raceStatus,
+ raceStartTime,
+ windDirection,
+ windSpeedKnots,
+ raceType,
+ boatStatuses );
+
+
+ byte[] encodedRaceStatus = RaceVisionByteEncoder.encode(raceStatusOriginal);
+ RaceStatusDecoder decoderTest = new RaceStatusDecoder();
+ decoderTest.decode(encodedRaceStatus);
- byte[] encodedRaceStatus = RaceVisionByteEncoder.raceStatus(raceStatusObject);
+ RaceStatus decodedMessage = decoderTest.getMessage();
- RaceStatusDecoder decoderTest = new RaceStatusDecoder(encodedRaceStatus);
+ compareRaceStatusMessages(raceStatusOriginal, decodedMessage);
- Assert.assertEquals(0b10, decoderTest.getVersionNum());
- Assert.assertEquals(time, decoderTest.getTime());
- Assert.assertEquals(raceID, decoderTest.getRace());
- Assert.assertEquals(raceStatus, decoderTest.getRaceState());
- Assert.assertEquals(raceStartTime, decoderTest.getStartTime());
- Assert.assertEquals(windDirection, decoderTest.getRaceWindDir());
- Assert.assertEquals(windSpeed, decoderTest.getRaceWindSpeed());
+ }
+
+
+ /**
+ * Compares two RaceStatus messages to check that they are equal.
+ * @param original The original RaceStatus message.
+ * @param decoded The decoded RaceStatus message.
+ */
+ public static void compareRaceStatusMessages(RaceStatus original, RaceStatus decoded) {
+
+ //Compare RaceStatus body.
+ Assert.assertEquals(original.getMessageVersionNumber(), decoded.getMessageVersionNumber());
+ Assert.assertEquals(original.getCurrentTime(), decoded.getCurrentTime());
+ Assert.assertEquals(original.getRaceID(), decoded.getRaceID());
+ Assert.assertEquals(original.getRaceStatus(), decoded.getRaceStatus());
+ Assert.assertEquals(original.getExpectedStartTime(), decoded.getExpectedStartTime());
+ Assert.assertEquals(original.getWindDirection().degrees(), decoded.getWindDirection().degrees(), 0.01);
+ Assert.assertEquals(original.getWindSpeed(), decoded.getWindSpeed(), 0.01);
+ //Compare all BoatStatuses
+ Iterator originalIterator = original.getBoatStatuses().iterator();
+ Iterator decodedIterator = decoded.getBoatStatuses().iterator();
- BoatStatus boat1 = decoderTest.getBoats().get(0);
+ while (originalIterator.hasNext() && decodedIterator.hasNext()) {
- Assert.assertEquals(boat1SourceID, boat1.getSourceID());
- Assert.assertEquals(boat1Status, boat1.getBoatStatus());
- Assert.assertEquals(boat1LegNumber, boat1.getLegNumber());
- Assert.assertEquals(boat1PenaltiesAwarded, boat1.getNumPenaltiesAwarded());
- Assert.assertEquals(boat1PenaltiesServed, boat1.getNumPenaltiesServed());
- Assert.assertEquals(boat1TimeAtNextMark, boat1.getEstTimeAtNextMark());
- Assert.assertEquals(boat1TimeAtFinish, boat1.getEstTimeAtFinish());
+ BoatStatusDecoderTest.compareBoatStatusMessages(originalIterator.next(), decodedIterator.next());
+
+ }
}
+
+
}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/RequestToJoinDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/RequestToJoinDecoderTest.java
new file mode 100644
index 00000000..fe70a709
--- /dev/null
+++ b/racevisionGame/src/test/java/network/MessageDecoders/RequestToJoinDecoderTest.java
@@ -0,0 +1,89 @@
+package network.MessageDecoders;
+
+import network.Exceptions.InvalidMessageException;
+import network.MessageEncoders.RaceVisionByteEncoder;
+import network.Messages.Enums.RequestToJoinEnum;
+import network.Messages.RequestToJoin;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Test for the RequestToJoin encoder and decoder
+ */
+public class RequestToJoinDecoderTest {
+
+
+ /**
+ * Encodes and decodes a given message.
+ * @param message Message to encode/decode.
+ * @return The decoded message.
+ * @throws InvalidMessageException If the message cannot be encoded.
+ */
+ private RequestToJoin encodeDecodeMessage(RequestToJoin message) throws InvalidMessageException {
+
+ //Encode.
+ byte [] testEncodedMessage = RaceVisionByteEncoder.encode(message);
+
+ //Decode.
+ RequestToJoinDecoder testDecoder = new RequestToJoinDecoder();
+ testDecoder.decode(testEncodedMessage);
+ RequestToJoin decodedMessage = testDecoder.getMessage();
+
+ return decodedMessage;
+ }
+
+
+ /**
+ * Tests if a specific request type message can be encoded and decoded correctly.
+ * @param type The type of join request.
+ * @throws Exception if test fails.
+ */
+ private void requestTypeTest(RequestToJoinEnum type) throws Exception {
+
+ //Prepare message.
+ RequestToJoin beforeMessage = new RequestToJoin(type);
+
+
+ //Encode/decode it.
+ RequestToJoin afterMessage = encodeDecodeMessage(beforeMessage);
+
+
+ //Compare.
+ assertEquals(beforeMessage.getRequestType().getValue(), afterMessage.getRequestType().getValue());
+
+ }
+
+
+ /**
+ * Tests if a spectator request message can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void spectatorTest() throws Exception {
+ requestTypeTest(RequestToJoinEnum.SPECTATOR);
+ }
+
+ /**
+ * Tests if a participant request message can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void participantTest() throws Exception {
+ requestTypeTest(RequestToJoinEnum.PARTICIPANT);
+ }
+
+ /**
+ * Tests if a ghost request message can be encoded and decoded correctly.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void ghostTest() throws Exception {
+ requestTypeTest(RequestToJoinEnum.GHOST);
+ }
+
+
+
+}
diff --git a/racevisionGame/src/test/java/network/MessageDecoders/XMLMessageDecoderTest.java b/racevisionGame/src/test/java/network/MessageDecoders/XMLMessageDecoderTest.java
index b0d0eee3..a71f3f34 100644
--- a/racevisionGame/src/test/java/network/MessageDecoders/XMLMessageDecoderTest.java
+++ b/racevisionGame/src/test/java/network/MessageDecoders/XMLMessageDecoderTest.java
@@ -1,5 +1,6 @@
package network.MessageDecoders;
+import network.Exceptions.InvalidMessageException;
import network.MessageEncoders.RaceVisionByteEncoder;
import network.Messages.Enums.XMLMessageType;
import network.Messages.XMLMessage;
@@ -14,13 +15,21 @@ import java.nio.charset.StandardCharsets;
import static org.junit.Assert.fail;
/**
- * Created by hba56 on 20/04/17.
+ * Test for the XMLMessage encoder and decoder
*/
public class XMLMessageDecoderTest {
- @Test
- public void getByteArrayTest(){
- try{
- String xmlString = XMLReader.readXMLFileToString("network/raceXML/Regatta.xml", StandardCharsets.UTF_8);
+
+
+ /**
+ * Creates an XML message of the given type, using the specified filePath, encodes it, decodes it, and checks that the result matches the starting message.
+ * @param filePath The file path for xml file.
+ * @param type The type of xml file.
+ * @throws InvalidMessageException Thrown if message cannot be encoded.
+ */
+ private void xmlMessageTest(String filePath, XMLMessageType type) throws InvalidMessageException {
+
+ try {
+ String xmlString = XMLReader.readXMLFileToString(filePath, StandardCharsets.UTF_8);
long time = System.currentTimeMillis();
@@ -28,27 +37,71 @@ public class XMLMessageDecoderTest {
(byte)1,
1,
time,
- XMLMessageType.REGATTA,
+ type,
(short)1,
xmlString );
- byte[] encodedXML = RaceVisionByteEncoder.xmlMessage(message);
+ byte[] encodedXML = RaceVisionByteEncoder.encode(message);
+
+ XMLMessageDecoder decoderXML = new XMLMessageDecoder();
+ decoderXML.decode(encodedXML);
+ XMLMessage decodedMessage = decoderXML.getMessage();
- XMLMessageDecoder decoderXML = new XMLMessageDecoder(encodedXML);
- decoderXML.decode();
+ compareXMLMessages(message, decodedMessage);
- Assert.assertEquals((byte)1, decoderXML.getMessageVersionNumber());
- Assert.assertEquals((short)1, decoderXML.getAckNumber());
- Assert.assertEquals(time, decoderXML.getTimeStamp());
- Assert.assertEquals(XMLMessageType.REGATTA, decoderXML.getXmlMsgSubType());
- Assert.assertEquals((short)1, decoderXML.getSequenceNumber());
- Assert.assertEquals((short)xmlString.length(), decoderXML.getXmlMsgLength());
- } catch (XMLReaderException | TransformerException e){
+ } catch (XMLReaderException e){
fail("couldn't read file" + e.getMessage());
}
+
+ }
+
+ /**
+ * Compares two XML messages to check that they are the same.
+ * @param originalMessage The first message to test.
+ * @param decodedMessage The second message to test.
+ */
+ public static void compareXMLMessages(XMLMessage originalMessage, XMLMessage decodedMessage) {
+
+ Assert.assertEquals(originalMessage.getVersionNumber(), decodedMessage.getVersionNumber());
+ Assert.assertEquals(originalMessage.getAckNumber(), decodedMessage.getAckNumber());
+ Assert.assertEquals(originalMessage.getTimeStamp(), decodedMessage.getTimeStamp());
+ Assert.assertEquals(originalMessage.getXmlMsgSubType(), decodedMessage.getXmlMsgSubType());
+ Assert.assertEquals(originalMessage.getSequenceNumber(), decodedMessage.getSequenceNumber());
+ Assert.assertEquals(originalMessage.getXmlMsgLength(), decodedMessage.getXmlMsgLength());
+ Assert.assertEquals(originalMessage.getXmlMessage(), decodedMessage.getXmlMessage());
+
+ }
+
+ /**
+ * Tests if a regatta.xml message can be encoded and decoded.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void regattaXMLMessageTest() throws Exception {
+ xmlMessageTest("network/raceXML/Regatta.xml", XMLMessageType.REGATTA);
+ }
+
+
+ /**
+ * Tests if a race.xml message can be encoded and decoded.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void raceXMLMessageTest() throws Exception {
+ xmlMessageTest("network/raceXML/Race.xml", XMLMessageType.RACE);
+ }
+
+
+ /**
+ * Tests if a boat.xml message can be encoded and decoded.
+ * @throws Exception if test fails.
+ */
+ @Test
+ public void boatXMLMessageTest() throws Exception {
+ xmlMessageTest("network/raceXML/Boats.xml", XMLMessageType.BOAT);
}
}
diff --git a/racevisionGame/src/test/java/network/Utils/AC35UnitConverterTest.java b/racevisionGame/src/test/java/network/Utils/AC35UnitConverterTest.java
index 24fc2be2..17369098 100644
--- a/racevisionGame/src/test/java/network/Utils/AC35UnitConverterTest.java
+++ b/racevisionGame/src/test/java/network/Utils/AC35UnitConverterTest.java
@@ -2,42 +2,92 @@ package network.Utils;
import org.junit.Test;
-import static network.Utils.AC35UnitConverter.convertGPS;
-import static network.Utils.AC35UnitConverter.convertGPSToInt;
-import static network.Utils.AC35UnitConverter.convertHeading;
-import static network.Utils.AC35UnitConverter.convertTrueWindAngle;
+import static network.Utils.AC35UnitConverter.*;
+import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
- * Created by fwy13 on 4/05/17.
+ * Tests the pack/unpack and conversion functions in {@link AC35UnitConverter}.
*/
public class AC35UnitConverterTest {
+
+ /**
+ * Tests if gps coordinates can be unpacked.
+ */
+ @Test
+ public void testUnpackGPS(){
+ assertTrue(unpackGPS(0) == 0);
+ assertTrue(unpackGPS(Integer.MAX_VALUE) == (double)Integer.MAX_VALUE * 180.0 / Math.pow(2, 31));
+
+ }
+
+
+ /**
+ * Tests if gps coodinates can be packed.
+ */
@Test
- public void testConvertGPS(){
- assertTrue(convertGPS(0) == 0);
- assertTrue(convertGPS(Integer.MAX_VALUE) == (double)Integer.MAX_VALUE * 180.0 / Math.pow(2, 31));
+ public void testPackGPS(){
+ assertTrue(packGPS(0) == 0);
+ assertTrue(packGPS(180) == (int)2147483648.0);
}
+ /**
+ * Tests if headings/bearings can be unpacked.
+ */
+ @Test
+ public void testUnpackHeading(){
+ assertTrue(unpackHeading(0) == 0);
+ assertTrue(unpackHeading(65536) == 360.0);
+ }
+
+ /**
+ * Tests if headings/bearings can be packed.
+ */
@Test
- public void testConvertGPSToInt(){
- assertTrue(convertGPSToInt(0) == 0);
- assertTrue(convertGPSToInt(180) == (int)2147483648.0);
+ public void testPackHeading(){
+ assertTrue(packHeading(0) == 0);
+ assertTrue(packHeading(360) == 65536);
+ }
+
+ /**
+ * Tests if true wind angles (azimuths) can be unpacked.
+ */
+ @Test
+ public void testUnpackTrueWindAngle(){
+ assertEquals(unpackTrueWindAngle((short)0), 0, 0.001);
+ assertEquals(unpackTrueWindAngle((short)32767), 180.0, 0.01);
}
+ /**
+ * Tests if true wind angles (azimuths) can be packed.
+ */
@Test
- public void testConvertHeading(){
- assertTrue(convertHeading(0) == 0);
- assertTrue(convertHeading(65536) == 360.0);
+ public void testPackTrueWindAngle(){
+ assertTrue(packTrueWindAngle(0) == (short)0);
+ assertTrue(packTrueWindAngle(180.0) == (short)32768);
}
+ /**
+ * Tests if millimeters per second can be unpacked to knots.
+ */
@Test
- public void testConvertTrueWindAngle(){
- assertTrue(convertTrueWindAngle(0) == 0);
- assertTrue(convertTrueWindAngle(32768) == 180.0);
+ public void testUnpackMMperSecToKnots(){
+ assertEquals(unpackMMperSecToKnots(0), 0d, 0.001);
+ assertEquals(unpackMMperSecToKnots(7331), 14.25, 0.01);
}
+
+ /**
+ * Tests if knots can be packed into millimeters per second.
+ */
+ @Test
+ public void testPackKnotsToMMperSec(){
+ assertEquals(packKnotsToMMperSec(0), 0, 1);
+ assertEquals(packKnotsToMMperSec(7.44), 3828, 1);
+ }
+
}
diff --git a/racevisionGame/src/test/java/network/XMLMessageEncoderTest.java b/racevisionGame/src/test/java/network/XMLMessageEncoderTest.java
deleted file mode 100644
index 44a99997..00000000
--- a/racevisionGame/src/test/java/network/XMLMessageEncoderTest.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package network;
-
-import network.MessageEncoders.RaceVisionByteEncoder;
-import network.Messages.Enums.XMLMessageType;
-import network.Messages.XMLMessage;
-import org.junit.Assert;
-import org.junit.Test;
-import shared.dataInput.XMLReader;
-import shared.exceptions.XMLReaderException;
-
-import javax.xml.transform.TransformerException;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-
-import static org.junit.Assert.fail;
-
-/**
- * Created by hba56 on 19/04/17.
- */
-public class XMLMessageEncoderTest {
- @Test
- public void getByteArrayTest() {
- try {
-
- String xmlString = XMLReader.readXMLFileToString("network/raceXML/Regatta.xml", StandardCharsets.UTF_8);
-
-
- XMLMessage message = new XMLMessage(
- (byte)1,
- 1,
- System.currentTimeMillis(),
- XMLMessageType.REGATTA,
- (short)1,
- xmlString );
-
- int xmlMessageLength = xmlString.getBytes().length;
-
- byte[] encodedXML = RaceVisionByteEncoder.xmlMessage(message);
-
- //1 + 2 + 6 + 1 + 2 + 2 + xml.byteLength
- Assert.assertEquals(14 + xmlMessageLength, encodedXML.length);
-
- } catch (XMLReaderException | TransformerException e){
- fail("couldn't read file" + e.getMessage());
- }
- }
-
-}
diff --git a/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java b/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java
index 19370be0..029447d6 100644
--- a/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java
+++ b/racevisionGame/src/test/java/shared/dataInput/BoatXMLReaderTest.java
@@ -31,31 +31,40 @@ public class BoatXMLReaderTest {
private List marks;
- @Before
- public void setUp() {
+ public static BoatDataSource createBoatDataSource() throws InvalidBoatDataException {
String boatXMLString = null;
try {
boatXMLString = XMLReader.readXMLFileToString("mock/mockXML/boatTest.xml", StandardCharsets.UTF_8);
- } catch (Exception e) {
- e.printStackTrace();
- fail("could not read boat xml file into a string");
+ } catch (XMLReaderException e) {
+ throw new InvalidBoatDataException("Could not read boat XML file into a string.", e);
+
}
try {
- boatData = new BoatXMLReader(boatXMLString, XMLFileType.Contents);
+ BoatDataSource boatData = new BoatXMLReader(boatXMLString, XMLFileType.Contents);
+ return boatData;
- boats = new ArrayList<>(boatData.getBoats().values());
- marks = new ArrayList<>(boatData.getMarkerBoats().values());
- } catch (XMLReaderException | InvalidBoatDataException e) {
- e.printStackTrace();
- assertTrue(false);
+ } catch (XMLReaderException e) {
+ throw new InvalidBoatDataException("Could not parse boat XML file.", e);
}
+
+
+ }
+
+ @Before
+ public void setUp() throws InvalidBoatDataException {
+
+ boatData = createBoatDataSource();
+
+ boats = new ArrayList<>(boatData.getBoats().values());
+ marks = new ArrayList<>(boatData.getMarkerBoats().values());
+
}
@Test
diff --git a/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java b/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java
index 0539a990..1f4e72f6 100644
--- a/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java
+++ b/racevisionGame/src/test/java/shared/dataInput/RaceXMLReaderTest.java
@@ -1,5 +1,39 @@
package shared.dataInput;
+import shared.enums.XMLFileType;
+import shared.exceptions.InvalidRaceDataException;
+import shared.exceptions.XMLReaderException;
+
+import javax.xml.transform.TransformerException;
+import java.nio.charset.StandardCharsets;
+
public class RaceXMLReaderTest {
-//TODO
+
+
+
+ public static RaceDataSource createRaceDataSource() throws InvalidRaceDataException {
+
+ String raceXMLString = null;
+
+ try {
+ raceXMLString = XMLReader.readXMLFileToString("mock/mockXML/raceTest.xml", StandardCharsets.UTF_8);
+
+ } catch (XMLReaderException e) {
+ throw new InvalidRaceDataException("Could not read race XML file into a string.", e);
+
+ }
+
+
+ try {
+
+ RaceXMLReader raceData = new RaceXMLReader(raceXMLString, XMLFileType.Contents);
+ return raceData;
+
+
+ } catch (XMLReaderException e) {
+ throw new InvalidRaceDataException("Could not parse race XML file.", e);
+ }
+
+
+ }
}
diff --git a/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java b/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java
index ad9bcd95..d406af32 100644
--- a/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java
+++ b/racevisionGame/src/test/java/shared/dataInput/RegattaXMLReaderTest.java
@@ -3,6 +3,11 @@ package shared.dataInput;
import org.junit.Before;
import org.junit.Test;
import shared.enums.XMLFileType;
+import shared.exceptions.InvalidRegattaDataException;
+import shared.exceptions.XMLReaderException;
+
+import javax.xml.transform.TransformerException;
+import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -14,6 +19,34 @@ import static org.junit.Assert.fail;
public class RegattaXMLReaderTest {
private RegattaXMLReader regatta;
+
+ public static RegattaDataSource createRegattaDataSource() throws InvalidRegattaDataException {
+
+ String regattaXMLString = null;
+
+ try {
+ regattaXMLString = XMLReader.readXMLFileToString("mock/mockXML/regattaTest.xml", StandardCharsets.UTF_8);
+
+ } catch (XMLReaderException e) {
+ throw new InvalidRegattaDataException("Could not read regatta XML file into a string.", e);
+
+ }
+
+
+ try {
+
+ RegattaDataSource raceData = new RegattaXMLReader(regattaXMLString, XMLFileType.Contents);
+ return raceData;
+
+
+ } catch (XMLReaderException e) {
+ throw new InvalidRegattaDataException("Could not parse regatta XML file.", e);
+ }
+
+
+ }
+
+
@Before
public void findFile() {
try {
diff --git a/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java
new file mode 100644
index 00000000..64fdfcb5
--- /dev/null
+++ b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerParticipantTest.java
@@ -0,0 +1,157 @@
+package visualiser.network;
+
+import mock.model.commandFactory.Command;
+import network.Messages.AC35Data;
+import network.Messages.Enums.JoinAcceptanceEnum;
+import network.Messages.Enums.RequestToJoinEnum;
+import network.Messages.JoinAcceptance;
+import org.junit.Before;
+import org.junit.Test;
+import visualiser.Commands.ConnectionToServerCommands.JoinSuccessParticipantCommand;
+import visualiser.Commands.ConnectionToServerCommands.JoinFailureCommand;
+import visualiser.Commands.ConnectionToServerCommands.ServerFullCommand;
+import visualiser.enums.ConnectionToServerState;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests the {@link ConnectionToServer} class with a Participant request, and how it reacts to various commands.
+ */
+public class ConnectionToServerParticipantTest {
+
+ private ConnectionToServer connectionToServer;
+ private Thread connectionToServerThread;
+
+ private BlockingQueue outgoingMessages;
+ private BlockingQueue incomingCommands;
+
+
+ @Before
+ public void setUp() throws Exception {
+
+ incomingCommands = new LinkedBlockingQueue<>();
+ outgoingMessages = new LinkedBlockingQueue<>();
+
+ connectionToServer = new ConnectionToServer(ConnectionToServerState.UNKNOWN, RequestToJoinEnum.PARTICIPANT, incomingCommands, outgoingMessages);
+ connectionToServerThread = new Thread(connectionToServer);
+ connectionToServerThread.start();
+
+ }
+
+
+ /**
+ * When a connection to server is created, is it expected that it will have sent a request and be in the Request_sent state.
+ * @throws Exception On error.
+ */
+ @Test
+ public void expectRequestSent() throws Exception {
+
+ //Need to wait for connection thread to execute commands.
+ Thread.sleep(250);
+
+ assertEquals(ConnectionToServerState.REQUEST_SENT, connectionToServer.getConnectionState());
+ }
+
+
+ /**
+ * When the connection to server thread is interrupted, it is expected the connection state will become TimedOut.
+ * @throws Exception On error.
+ */
+ @Test
+ public void interruptTimedOut() throws Exception {
+
+ //Need to wait for connection thread to execute commands.
+ Thread.sleep(250);
+
+
+ //Disable logging as we know this will log but we don't care.
+ Logger.getGlobal().setLevel(Level.OFF);
+ connectionToServerThread.interrupt();
+ Logger.getGlobal().setLevel(null);
+
+ connectionToServerThread.join();
+
+ assertEquals(ConnectionToServerState.TIMED_OUT, connectionToServer.getConnectionState());
+ }
+
+
+ /**
+ * Sends a join successful command. Expects that the connection becomes Connected.
+ * @throws Exception On error.
+ */
+ @Test
+ public void sendJoinSuccessCommand() throws Exception {
+ int sourceID = 123;
+ JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, sourceID);
+
+ Command command = new JoinSuccessParticipantCommand(joinAcceptance, connectionToServer);
+
+ incomingCommands.put(command);
+
+ //Need to wait for connection thread to execute commands.
+ Thread.sleep(250);
+
+ assertEquals(ConnectionToServerState.CONNECTED, connectionToServer.getConnectionState());
+ assertTrue(connectionToServer.getJoinAcceptance() != null);
+ assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID());
+ assertNotEquals(0, connectionToServer.getJoinAcceptance().getSourceID());
+ assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, connectionToServer.getJoinAcceptance().getAcceptanceType());
+
+
+ }
+
+
+ /**
+ * Sends a participants full command. Expects that the connection becomes Declined.
+ * @throws Exception On error.
+ */
+ @Test
+ public void sendJoinFailureCommand() throws Exception {
+ int sourceID = 0;
+ JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_FAILURE, sourceID);
+
+ Command command = new JoinFailureCommand(joinAcceptance, connectionToServer);
+
+ incomingCommands.put(command);
+
+ //Need to wait for connection thread to execute commands.
+ Thread.sleep(250);
+
+ assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState());
+ assertTrue(connectionToServer.getJoinAcceptance() != null);
+ assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID());
+ assertEquals(JoinAcceptanceEnum.JOIN_FAILURE, connectionToServer.getJoinAcceptance().getAcceptanceType());
+ }
+
+
+ /**
+ * Sends a server full command. Expects that the connection becomes Declined.
+ * @throws Exception On error.
+ */
+ @Test
+ public void sendServerFullCommand() throws Exception {
+ int sourceID = 0;
+ JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.SERVER_FULL, sourceID);
+
+ Command command = new ServerFullCommand(joinAcceptance, connectionToServer);
+
+ incomingCommands.put(command);
+
+ //Need to wait for connection thread to execute commands.
+ Thread.sleep(250);
+
+ assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState());
+ assertTrue(connectionToServer.getJoinAcceptance() != null);
+ assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID());
+ assertEquals(JoinAcceptanceEnum.SERVER_FULL, connectionToServer.getJoinAcceptance().getAcceptanceType());
+ }
+
+
+}
+
+
diff --git a/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java
new file mode 100644
index 00000000..c381dc88
--- /dev/null
+++ b/racevisionGame/src/test/java/visualiser/network/ConnectionToServerSpectatorTest.java
@@ -0,0 +1,108 @@
+package visualiser.network;
+
+import mock.model.commandFactory.Command;
+import network.Messages.AC35Data;
+import network.Messages.Enums.JoinAcceptanceEnum;
+import network.Messages.Enums.RequestToJoinEnum;
+import network.Messages.JoinAcceptance;
+import org.junit.Before;
+import org.junit.Test;
+import visualiser.Commands.ConnectionToServerCommands.JoinSuccessParticipantCommand;
+import visualiser.Commands.ConnectionToServerCommands.ServerFullCommand;
+import visualiser.enums.ConnectionToServerState;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests the {@link ConnectionToServer} class with a Spectator request, and how it reacts to various commands.
+ */
+public class ConnectionToServerSpectatorTest {
+
+ private ConnectionToServer connectionToServer;
+ private Thread connectionToServerThread;
+
+ private BlockingQueue outgoingMessages;
+ private BlockingQueue incomingCommands;
+
+
+ @Before
+ public void setUp() throws Exception {
+
+ incomingCommands = new LinkedBlockingQueue<>();
+ outgoingMessages = new LinkedBlockingQueue<>();
+
+ connectionToServer = new ConnectionToServer(ConnectionToServerState.UNKNOWN, RequestToJoinEnum.SPECTATOR, incomingCommands, outgoingMessages);
+ connectionToServerThread = new Thread(connectionToServer);
+ connectionToServerThread.start();
+
+ }
+
+
+ /**
+ * When a connection to server is created, is it expected that it will have sent a request and be in the Request_sent state.
+ * @throws Exception On error.
+ */
+ @Test
+ public void expectRequestSent() throws Exception {
+
+ //Need to wait for connection thread to execute commands.
+ Thread.sleep(250);
+
+ assertEquals(ConnectionToServerState.REQUEST_SENT, connectionToServer.getConnectionState());
+ }
+
+
+ /**
+ * Sends a join successful command. Expects that the connection becomes Connected.
+ * @throws Exception On error.
+ */
+ @Test
+ public void sendJoinSuccessCommand() throws Exception {
+ int sourceID = 0;
+ JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, sourceID);
+
+ Command command = new JoinSuccessParticipantCommand(joinAcceptance, connectionToServer);
+
+ incomingCommands.put(command);
+
+ //Need to wait for connection thread to execute commands.
+ Thread.sleep(250);
+
+ assertEquals(ConnectionToServerState.CONNECTED, connectionToServer.getConnectionState());
+ assertTrue(connectionToServer.getJoinAcceptance() != null);
+ assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID());
+ assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, connectionToServer.getJoinAcceptance().getAcceptanceType());
+
+
+ }
+
+
+ /**
+ * Sends a server full command. Expects that the connection becomes Declined.
+ * @throws Exception On error.
+ */
+ @Test
+ public void sendServerFullCommand() throws Exception {
+ int sourceID = 0;
+ JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.SERVER_FULL, sourceID);
+
+ Command command = new ServerFullCommand(joinAcceptance, connectionToServer);
+
+ incomingCommands.put(command);
+
+ //Need to wait for connection thread to execute commands.
+ Thread.sleep(250);
+
+ assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState());
+ assertTrue(connectionToServer.getJoinAcceptance() != null);
+ assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID());
+ assertEquals(JoinAcceptanceEnum.SERVER_FULL, connectionToServer.getJoinAcceptance().getAcceptanceType());
+ }
+
+
+}
+
+
diff --git a/racevisionGame/src/test/resources/network/raceXML/Boats.xml b/racevisionGame/src/test/resources/network/raceXML/Boats.xml
new file mode 100644
index 00000000..ed4b6ded
--- /dev/null
+++ b/racevisionGame/src/test/resources/network/raceXML/Boats.xml
@@ -0,0 +1,119 @@
+
+
+
+ 2017-04-19T15:49:40+1200
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/racevisionGame/src/test/resources/network/raceXML/Race.xml b/racevisionGame/src/test/resources/network/raceXML/Race.xml
new file mode 100644
index 00000000..29478c60
--- /dev/null
+++ b/racevisionGame/src/test/resources/network/raceXML/Race.xml
@@ -0,0 +1,58 @@
+
+
+ 17041901
+ Fleet
+ 2017-04-19T15:30:00+1200
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file