Merge branch 'master' into story_64_sails

# Conflicts:
#	racevisionGame/src/main/java/mock/app/MockOutput.java
#	racevisionGame/src/main/java/mock/model/MockRace.java
#	racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java
#	racevisionGame/src/main/java/network/MessageDecoders/BoatActionDecoder.java
#	racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java
#	racevisionGame/src/main/java/visualiser/app/VisualiserInput.java
#	racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java
#	racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java
#	racevisionGame/src/main/java/visualiser/model/VisualiserRace.java
#	racevisionGame/src/main/resources/visualiser/scenes/connect.fxml
#	racevisionGame/src/main/resources/visualiser/scenes/lobby.fxml
#	racevisionGame/src/test/java/mock/model/MockRaceTest.java
#	racevisionGame/src/test/java/network/Utils/AC35UnitConverterTest.java
#	racevisionGame/src/test/java/network/XMLMessageEncoderTest.java
main
Joseph 8 years ago
commit f60809bdd4

1
.gitignore vendored

@ -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

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectCodeStyleSettingsManager">
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</component>
</project>

@ -83,89 +83,40 @@
</properties>
<profiles>
<profile>
<id>mock</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>visualiser.app.App</Main-Class>
<X-Compile-Source-JDK>${maven.compiler.source}</X-Compile-Source-JDK>
<X-Compile-Target-JDK>${maven.compiler.target}</X-Compile-Target-JDK>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>visualiser</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>visualiser.app.App</Main-Class>
<X-Compile-Source-JDK>${maven.compiler.source}</X-Compile-Source-JDK>
<X-Compile-Target-JDK>${maven.compiler.target}</X-Compile-Target-JDK>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>visualiser.app.App</Main-Class>
<X-Compile-Source-JDK>${maven.compiler.source}</X-Compile-Source-JDK>
<X-Compile-Target-JDK>${maven.compiler.target}</X-Compile-Target-JDK>
</manifestEntries>
</transformer>
</transformers>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

@ -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<MockOutput> mockOutputList = new ArrayBlockingQueue<>(16, true);
//latest messages
/**
* List of client connections.
*/
private BlockingQueue<ClientConnection> 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,29 +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();
System.out.println("I'm in connectionAcceptor");
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);
}
}
@ -100,14 +142,14 @@ public class ConnectionAcceptor implements Runnable {
*/
class CheckClientConnection implements Runnable{
private ArrayBlockingQueue<MockOutput> mocks;
private BlockingQueue<ClientConnection> connections;
/**
* Constructor
* @param mocks Mocks "connected"
* @param connections Clients "connected"
*/
public CheckClientConnection(ArrayBlockingQueue<MockOutput> mocks){
this.mocks = mocks;
public CheckClientConnection(BlockingQueue<ClientConnection> connections){
this.connections = connections;
}
/**
@ -115,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<MockOutput> 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<ClientConnection, Integer> connectionDeadCount = new HashMap<>();
while(!Thread.interrupted()) {
//Make copy of connections.
List<ClientConnection> 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;
}
}
}

@ -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,71 +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 {
//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);
new Thread(mockOutput).start();
sendXMLs();
} catch (XMLReaderException | InvalidRaceDataException | InvalidRegattaDataException | InvalidBoatDataException e) {
throw new EventConstructionException("Could not parse XML files.", e);
//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);
}
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);
}
mockOutput.setRace(newRace);
new Thread(connectionAcceptor, "Event.Start()->ConnectionAcceptor thread").start();
sendXMLs();
}
/**
* Sends the initial race data and then begins race simulation.
*/
public void start() {
new Thread(newRace).start();
System.out.println("I'm in event");
}
/**
@ -107,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);
}
/**
@ -122,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;

@ -1,34 +1,26 @@
package mock.app;
import network.BinaryMessageEncoder;
import network.MessageEncoders.RaceVisionByteEncoder;
import network.Messages.*;
import network.Messages.Enums.MessageType;
import shared.model.RunnableWithFramePeriod;
import java.io.DataOutputStream;
import java.io.IOException;
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<AC35Data> outgoingMessages;
/**
@ -39,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<AC35Data> 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()) {
try {
Thread.sleep(500);
while(true) {
try {
} 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);
long currentFrameTime = System.currentTimeMillis();
//Re-set the interrupt flag.
Thread.currentThread().interrupt();
return;
//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.
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<AC35Data> 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;
}
}

@ -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<Byte, ConnectionStateEnum> 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;
}
}
}

@ -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);
}
}

@ -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);
}
}

@ -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);
}
}

@ -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<AC35Data> outputQueue;
/**
* Used to read messages from socket.
*/
private MessageDeserialiser messageDeserialiser;
/**
* Stores messages read from socket.
*/
private BlockingQueue<AC35Data> 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();
}
}
}

@ -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();
}
}

@ -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<AC35Data> 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<AC35Data> 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;
}
}
}
}

@ -75,7 +75,15 @@ public class MockBoat extends Boat {
//Get the start and end points.
GPSCoordinate currentPosition = this.getCurrentPosition();
GPSCoordinate nextMarkerPosition = this.getCurrentLeg().getEndCompoundMark().getAverageGPSCoordinate();
GPSCoordinate nextMarkerPosition;
// if boat is at the finish
if (this.getCurrentLeg().getEndCompoundMark() == null) {
nextMarkerPosition = currentPosition;
}
else {
nextMarkerPosition = this.getCurrentLeg().getEndCompoundMark().getAverageGPSCoordinate();
}
//Calculate bearing.
Bearing bearing = GPSCoordinate.calculateBearing(currentPosition, nextMarkerPosition);
@ -211,11 +219,22 @@ public class MockBoat extends Boat {
* @return true if mark is on port side
*/
public boolean isPortSide(Mark mark){
//if this boat is lower than the mark check which way it is facing
if(this.getCurrentPosition().getLongitude() < mark.getPosition().getLongitude()){
Bearing towardsMark = GPSCoordinate.calculateBearing(this.getCurrentPosition(), mark.getPosition());
if (towardsMark.degrees() > 315 || towardsMark.degrees() <= 45){
//south quadrant
return this.getBearing().degrees() <= 180;
} else if(towardsMark.degrees() > 45 && towardsMark.degrees() <= 135){
//west quadrant
return (this.getBearing().degrees() <= 270 && this.getBearing().degrees() >= 90);
}else if(towardsMark.degrees() > 135 && towardsMark.degrees() <= 225){
//north quadrant
return this.getBearing().degrees() >= 180;
}else if(towardsMark.degrees() > 225 && towardsMark.degrees() <= 315){
//east quadrant
return (this.getBearing().degrees() <= 90 || this.getBearing().degrees() >= 270);
}else{
return this.getBearing().degrees() > 180;
//should not reach here
return false;
}
}
@ -226,10 +245,22 @@ public class MockBoat extends Boat {
*/
public boolean isStarboardSide(Mark mark){
//if this boat is lower than the mark check which way it is facing
if(this.getCurrentPosition().getLongitude() < mark.getPosition().getLongitude()){
return this.getBearing().degrees() >= 180;
Bearing towardsMark = GPSCoordinate.calculateBearing(this.getCurrentPosition(), mark.getPosition());
if (towardsMark.degrees() > 315 || towardsMark.degrees() <= 45){
//south quadrant
return !(this.getBearing().degrees() <= 180);
} else if(towardsMark.degrees() > 45 && towardsMark.degrees() <= 135){
//west quadrant
return !(this.getBearing().degrees() <= 270 && this.getBearing().degrees() >= 90);
}else if(towardsMark.degrees() > 135 && towardsMark.degrees() <= 225){
//north quadrant
return !(this.getBearing().degrees() >= 180);
}else if(towardsMark.degrees() > 225 && towardsMark.degrees() <= 315){
//east quadrant
return !(this.getBearing().degrees() <= 90 || this.getBearing().degrees() >= 270);
}else{
return this.getBearing().degrees() < 180;
//should not reach here
return false;
}
}
@ -239,12 +270,19 @@ public class MockBoat extends Boat {
* @return true if the boat is between two marks that make up a gate
*/
public boolean isBetweenGate(CompoundMark gate){
if ((this.isPortSide(gate.getMark1()) && this.isStarboardSide(gate.getMark2())) ||
(this.isStarboardSide(gate.getMark2()) && this.isPortSide(gate.getMark1()))){
return true;
}else{
return false;
}
return (this.isPortSide(gate.getMark1()) && this.isStarboardSide(gate.getMark2())) ||
(this.isStarboardSide(gate.getMark1()) && this.isPortSide(gate.getMark2()));
}
/**
* Used to check if this boat is between a two marks
* @param mark1 the first mark
* @param mark2 the second mark
* @return true if the boat is between two marks
*/
public boolean isBetweenGate(Mark mark1, Mark mark2){
return (this.isPortSide(mark1) && this.isStarboardSide(mark2)) ||
(this.isStarboardSide(mark1) && this.isPortSide(mark2));
}
public Integer getRoundingStatus() {
@ -258,9 +296,7 @@ public class MockBoat extends Boat {
public void resetRoundingStatus() {
this.roundingStatus = 0;
}
public boolean isAutoVMG() {
return autoVMG;
}
public void setAutoVMG(boolean autoVMG) {
this.autoVMG = autoVMG;

@ -3,12 +3,12 @@ package mock.model;
import network.Messages.Enums.BoatStatusEnum;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.LatestMessages;
import org.opengis.geometry.primitive.*;
import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import shared.dataInput.RegattaDataSource;
import shared.exceptions.BoatNotFoundException;
import shared.enums.RoundingType;
import shared.model.*;
import shared.model.Bearing;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
@ -53,13 +53,13 @@ public class MockRace extends Race {
* @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}.
* @param windGenerator The wind generator used for the race.
*/
public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale) {
public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, Polars polars, int timeScale, WindGenerator windGenerator) {
super(boatDataSource, raceDataSource, regattaDataSource, latestMessages);
super(boatDataSource, raceDataSource, regattaDataSource);
this.scaleFactor = timeScale;
@ -67,14 +67,8 @@ public class MockRace extends Race {
this.shrinkBoundary = GPSCoordinate.getShrinkBoundary(this.boundary);
//Set up wind generator. It may be tidier to create this outside the race (with the values sourced from a data file maybe?) and pass it in.
this.windGenerator = new WindGenerator(
Bearing.fromDegrees(225),
Bearing.fromDegrees(215),
Bearing.fromDegrees(235),
12d,
8d,
16d );
this.windGenerator = windGenerator;
//Wind.
this.setWind(windGenerator.generateBaselineWind());
@ -319,7 +313,14 @@ public class MockRace extends Race {
//Checks if the current boat has finished the race or not.
boolean finish = this.isLastLeg(boat.getCurrentLeg());
if (!finish && totalElapsedMilliseconds >= updatePeriodMilliseconds & boat.isSailsOut()) {
if (!finish && totalElapsedMilliseconds >= updatePeriodMilliseconds) {
checkPosition(boat, totalElapsedMilliseconds);
if (boat.getCurrentSpeed() == 0) {
newOptimalVMG(boat);
boat.setBearing(boat.calculateBearingToNextMarker());
}
setBoatSpeed(boat);
@ -334,14 +335,12 @@ public class MockRace extends Race {
boat.moveForwards(distanceTravelledMeters);
boat.setTimeSinceTackChange(boat.getTimeSinceTackChange() + updatePeriodMilliseconds);
if (boat.isAutoVMG()) {
if (boat.getAutoVMG()) {
newOptimalVMG(boat);
}
} else {
boat.setCurrentSpeed(0);
}
this.updateEstimatedTime(boat);
this.updateEstimatedTime(boat);
}
}
@ -463,28 +462,38 @@ public class MockRace extends Race {
* 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<GPSCoordinate> roundingChecks){
private void boatRoundingCheckPort(MockBoat boat, List<GPSCoordinate> 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
// System.out.println("round 0");
if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) &&
GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(),
roundingChecks.get(0), boat.getCurrentPosition())) {
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
// System.out.println("round 1");
if (boat.isPortSide(boat.getCurrentLeg().getEndCompoundMark().getMark1()) &&
GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(),
roundingChecks.get(1), boat.getCurrentPosition())) {
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
// System.out.println("round 2");
//Move boat on to next leg.
boat.resetRoundingStatus();
Leg nextLeg = this.legs.get(boat.getCurrentLeg().getLegNumber() + 1);
@ -497,21 +506,34 @@ public class MockRace extends Race {
* 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<GPSCoordinate> roundingChecks){
private void boatRoundingCheckStarboard(MockBoat boat, List<GPSCoordinate> 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(boat.getCurrentLeg().getEndCompoundMark().getMark1()) &&
GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(),
roundingChecks.get(0), boat.getCurrentPosition())) {
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(boat.getCurrentLeg().getEndCompoundMark().getMark1()) &&
GPSCoordinate.intersects(boat.getCurrentLeg().getEndCompoundMark().getMark1().getPosition(),
roundingChecks.get(1), boat.getCurrentPosition())) {
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;
@ -530,37 +552,55 @@ public class MockRace extends Race {
* @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 = 250.0 / Constants.NMToMetersConversion; //250 meters.
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();
//todo will need to change this for gates, so that the end point is the side of the gate needed to be rounded
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
GPSCoordinate roundCheck1 = GPSCoordinate.calculateNewPosition(startDirectionLinePoint,
epsilonNauticalMiles, Azimuth.fromDegrees(bearingOfDirectionLine.degrees() + 90));//adding 90 so the check line is parallel
GPSCoordinate roundCheck2 = GPSCoordinate.calculateNewPosition(startDirectionLinePoint,
epsilonNauticalMiles, Azimuth.fromDegrees(bearingOfDirectionLine.degrees()));
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<GPSCoordinate> roundingChecks = new ArrayList<GPSCoordinate>(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);
boatRoundingCheckPort(boat, roundingChecks, bearingOfDirectionLine);
break;
case PS://not yet implemented so these gates will be rounded starboard side
case Starboard:
boatRoundingCheckStarboard(boat, roundingChecks);
boatRoundingCheckStarboard(boat, roundingChecks, bearingOfDirectionLine);
break;
}
@ -611,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].
*/
@ -643,7 +702,6 @@ public class MockRace extends Race {
}
public List<CompoundMark> getCompoundMarks() {
return compoundMarks;
}
}

@ -1,20 +1,16 @@
package mock.model;
import javafx.animation.AnimationTimer;
import mock.model.commandFactory.Command;
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;
import java.util.Stack;
public class RaceLogic implements Observer, Runnable {
public class RaceLogic implements RunnableWithFramePeriod {
/**
* State of current race modified by this object
*/
@ -30,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;
}
/**
@ -43,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);
@ -67,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() {
private void raceLoop() {
/**
* Start time of loop, in milliseconds.
*/
long timeRaceStarted = System.currentTimeMillis();
long previousFrameTime = System.currentTimeMillis();
/**
* Current time during a loop iteration.
*/
long currentTime = System.currentTimeMillis();
/**
* The time of the previous frame, in milliseconds.
*/
long lastFrameTime = timeRaceStarted;
long framePeriod = currentTime - lastFrameTime;
@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);
@ -127,14 +108,13 @@ 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());
}
@ -145,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.
@ -176,7 +151,7 @@ public class RaceLogic implements Observer, Runnable {
@Override
public void handle(long now) {
server.parseRaceStatus();
server.parseSnapshot();
if (iters > 500) {
stop();
@ -185,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;
}
}

@ -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<AC35Data> 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<BoatLocation> parseMarks() {
List<BoatLocation> 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<BoatLocation> parseBoatLocations() {
List<BoatLocation> 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<BoatStatus> 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;
}
}

@ -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;
}
}

@ -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<Integer> unallocatedIDs = new ArrayList<>();
/**
* This list contains all allocated source IDs.
*/
List<Integer> 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<Integer> 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);
}
}

@ -1,555 +0,0 @@
//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<BoatStatus> 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);
// }
//
// }
// }
//}

@ -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);
}

@ -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,19 +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);
case SAILS_OUT: return new SailsCommand(race, boat, true);
case SAILS_IN: return new SailsCommand(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.");
}
}
}

@ -2,31 +2,30 @@ package mock.model.commandFactory;
import mock.model.MockBoat;
import mock.model.MockRace;
import mock.model.VMG;
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() {
/*if(boat.getBearing().degrees()>180){
boat.setBearing(Bearing.fromDegrees(360 - race.getWindDirection().degrees()));
} else {
boat.setBearing(Bearing.fromDegrees(race.getWindDirection().degrees()));
}*/
/*double angle = Math.max(race.getWindDirection().degrees(), boat.getBearing().degrees()) - Math.min(race.getWindDirection().degrees(), boat.getBearing().degrees());
boat.setBearing(Bearing.fromDegrees(angle));*/
boat.setAutoVMG(false);
double boatAngle = boat.getBearing().degrees();
double windAngle =race.getWindDirection().degrees();
double differenceAngle = calcDistance(boatAngle, windAngle);
@ -39,10 +38,15 @@ public class TackGybeCommand implements Command {
}
}
/**
* 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;
double distance = phi > 180 ? 360 - phi : phi;
return distance;
return phi > 180 ? 360 - phi : phi;
}
}

@ -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() {
if (boat.getAutoVMG()){
boat.setAutoVMG(false);
System.out.println("Auto VMG off!");
} else {
boat.setAutoVMG(true);
System.out.println("Auto VMG on!");
}
}
}

@ -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));
}

@ -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;
@ -18,42 +19,71 @@ import static network.Utils.ByteConverter.bytesToLong;
*/
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;
@ -115,96 +145,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);
}

@ -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);
}
}

@ -0,0 +1,9 @@
package network.MessageControllers;
public class MessageController {
}

@ -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;
}
}

@ -1,18 +1,56 @@
package network.MessageDecoders;
import network.Exceptions.InvalidMessageException;
import network.Messages.AC35Data;
import network.Messages.BoatAction;
import network.Messages.Enums.BoatActionEnum;
public class BoatActionDecoder {
byte byteBoatAction;
BoatActionEnum boatAction;
import java.util.Arrays;
public BoatActionDecoder(byte[] encodedBoatAction) {
byteBoatAction = encodedBoatAction[0];
/**
* Decodes {@link BoatAction} messages.
*/
public class BoatActionDecoder implements MessageDecoder {
boatAction = BoatActionEnum.fromByte(byteBoatAction);
/**
* The encoded message.
*/
private byte[] encodedMessage;
/**
* The decoded message.
*/
private BoatAction message;
/**
* Constructs a decoder to decode a given message.
*/
public BoatActionDecoder() {
}
@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);
}
}
public BoatActionEnum getBoatAction() {
return boatAction;
/**
* Returns the decoded message.
* @return The decoded message.
*/
public BoatAction getMessage() {
return message;
}
}

@ -1,140 +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.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;
}

@ -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;
}
}

@ -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<CourseWind> 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<CourseWind> 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;
}
}

@ -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<CourseWind> 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;
}
}

@ -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);
}
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}

@ -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;
}
}

@ -1,119 +1,125 @@
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.*;
/**
* 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<BoatStatus> 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<BoatStatus> 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<BoatStatus> getBoats() {
return boats;
/**
* Returns the decoded message.
* @return The decoded message.
*/
public RaceStatus getMessage() {
return message;
}
}

@ -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;
}
}

@ -1,6 +1,9 @@
package network.MessageDecoders;
import network.Exceptions.InvalidMessageException;
import network.Messages.AC35Data;
import network.Messages.Enums.XMLMessageType;
import network.Messages.XMLMessage;
import java.util.Arrays;
@ -9,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;
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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;
}

@ -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);
}
}
}

@ -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<BoatStatus> 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);
}
}
}

@ -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 java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
@ -18,72 +22,8 @@ import static network.Utils.ByteConverter.longToBytes;
*/
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<BoatStatus> 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);
@ -126,25 +66,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;
@ -182,169 +103,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<CourseWind> 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;
}
}

@ -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);
}
}
}

@ -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);
}
}
}

@ -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<AC35Data> incomingMessages;
/**
* The routing map, which maps from a {@link MessageType} to a message queue.
*/
private Map<MessageType, BlockingQueue<AC35Data>> routeMap = new HashMap<>();
/**
* The default routing queue.
* Messages without routes are sent here.
* Nothing by default, which means unrouted messages are discarded
*/
private Optional<BlockingQueue<AC35Data>> defaultRoute = Optional.empty();
/**
* Constructs a {@link MessageRouter} with a given incoming message queue.
* @param incomingMessages Incoming message queue to read from.
*/
public MessageRouter(BlockingQueue<AC35Data> 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<AC35Data> 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<AC35Data> 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<AC35Data> 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<AC35Data> queue = routeMap.get(message.getType());
if (queue != null) {
queue.put(message);
} else {
//No route. Use default.
BlockingQueue<AC35Data> 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();
}
}
}
}

@ -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;
}
}

@ -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;
}
}

@ -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;
}
/**
* 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;
}
}

@ -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());

@ -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;
}

@ -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;
}
}

@ -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<CourseWind> courseWinds;
public CourseWinds(int msgVerNum, int selectedWindID, List<CourseWind> 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<CourseWind> 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<CourseWind> getCourseWinds() {
return courseWinds;
}
}

@ -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),

@ -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<Byte, BoatLocationDeviceEnum> 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;
}
}
}

@ -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<Byte, JoinAcceptanceEnum> 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;
}
}
}

@ -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<Byte, MarkRoundingBoatStatusEnum> 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;
}
}
}

@ -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<Byte, MarkRoundingIDEnum> 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;
}
}
}

@ -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<Byte, MarkRoundingSideEnum> 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;
}
}
}

@ -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<Byte, MarkRoundingTypeEnum> 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;
}
}
}

@ -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) {

@ -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<Byte, RaceStartTypeEnum> 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;
}
}
}

@ -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<Integer, RequestToJoinEnum> 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;
}
}
}

@ -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;
}

@ -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;
}
}

@ -2,9 +2,7 @@ package network.Messages;
import network.Messages.Enums.XMLMessageType;
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...).
@ -12,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<Integer, BoatStatus> boatStatusMap = new HashMap<>();
/**
* A map of the last BoatLocation message received, for each boat.
*/
private final Map<Integer, BoatLocation> boatLocationMap = new HashMap<>();
/**
* A map of the last MarkRounding message received, for each boat.
*/
private final Map<Integer, MarkRounding> 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<AC35Data> snapshot = new ArrayList<>();
/**
@ -68,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<AC35Data> 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<AC35Data> 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<Integer, BoatLocation> getBoatLocationMap() {
return boatLocationMap;
}
/**
* Returns the map of boat sourceIDs to BoatStatus messages.
* @return Map between boat sourceID and BoatStatus.
*/
public Map<Integer, BoatStatus> getBoatStatusMap() {
return boatStatusMap;
}
/**
* Returns the map of boat sourceIDs to MarkRounding messages.
* @return Map between boat sourceID and MarkRounding.
*/
public Map<Integer, MarkRounding> getMarkRoundingMap() {
return markRoundingMap;
}

@ -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;
}
}

@ -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<AC35Data> snapshot;
/**
* Constructs a snapshot using a given list of messages.
* @param snapshot Messages to use as snapshot.
*/
public RaceSnapshot(List<AC35Data> snapshot) {
this.snapshot = snapshot;
}
/**
* Gets the contents of the snapshot.
* This is a shallow copy.
* @return Contents of the snapshot.
*/
public List<AC35Data> getSnapshot() {
List<AC35Data> copy = new ArrayList<>(snapshot);
return copy;
}
}

@ -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;
}
}

@ -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<BoatStatus> boatStatuses;
public RaceStatus(long currentTime, int raceID, byte raceStatus, long expectedStartTime, int windDirection, int windSpeed, int raceType, List<BoatStatus> 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<BoatStatus> 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<BoatStatus> 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);
}
}

@ -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;
}
}

@ -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<AC35Data> 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<AC35Data> 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<AC35Data> 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;
}
}
}
}

@ -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<AC35Data> 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<AC35Data> 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<AC35Data> 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<AC35Data> 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;
}
}
}
}
}

@ -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);
}
}

@ -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<Integer, Boat> boatMap = new HashMap<>();
/**
* A map of source ID to mark for all marks in the race.
*/
private final Map<Integer, Mark> 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<Integer, Boat> 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<Integer, Mark> getMarkerBoats() {
return markerMap;
}
}

@ -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<GPSCoordinate> boundary = new ArrayList<>();
/**
* A map between compoundMarkID and a CompoundMark for all CompoundMarks in a race.
*/
private final Map<Integer, CompoundMark> compoundMarkMap = new HashMap<>();
/**
* A list of boat sourceIDs participating in the race.
*/
private final List<Integer> participants = new ArrayList<>();
/**
* A list of legs in the race.
*/
private final List<Leg> 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<GPSCoordinate> getBoundary() {
return boundary;
}
public GPSCoordinate getMapTopLeft() {
return mapTopLeft;
}
public GPSCoordinate getMapBottomRight() {
return mapBottomRight;
}
public List<Leg> getLegs() {
return legs;
}
public List<CompoundMark> 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<Integer> getParticipants() {
return participants;
}
}

@ -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);
}
}

@ -366,7 +366,7 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
CompoundMark currentCompoundMark = this.compoundMarkMap.get(cornerID);
//Sets the rounding type of this compound mark
currentCompoundMark.setRoundingType(RoundingType.valueOf(cornerRounding));
currentCompoundMark.setRoundingType(RoundingType.getValueOf(cornerRounding));
//Create a leg from these two adjacent compound marks.
Leg leg = new Leg(legName, lastCompoundMark, currentCompoundMark, i - 1);

@ -165,10 +165,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);
@ -181,7 +180,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);
}
}

@ -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);
}
}

@ -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);
}
}

@ -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);
}
}

@ -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;
/**
@ -110,6 +111,8 @@ public class Boat {
this.bearing = Bearing.fromDegrees(0d);
setCurrentPosition(new GPSCoordinate(0, 0));
this.status = BoatStatusEnum.UNDEFINED;
}
@ -369,6 +372,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;
}

@ -149,6 +149,19 @@ public class CompoundMark {
}
/**
* 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

@ -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();
}
}

@ -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
}
/**

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save