Story 61 [Agilefant link](http://agilefant.cosc.canterbury.ac.nz:8080/agilefant302/qrq.action?q=story:1095) ``` Note: the boat should be permanently circled with a highlight (or something similar) See: (http://learn.canterbury.ac.nz/mod/forum/discuss.php?d=213948). Contains boat request to join, and server response messages. Acceptance criteria: - The boat that responds to control keypresses is permanently highlighted on screen. - No boat without a highlight responds to keypresses. ``` Works with either single player or multiplayer game (see HostController.hostGamePressed()). The race.xml currently is not updated when players join/quit, so there are 6 boats by default, and any boats without playes will just sail off the side of the map. - resolves #39 - resolves #37 - resolves #35 - resolves #27 - resolves #25 - resolves #32 See merge request !26main
commit
3d953362a6
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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,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.
|
||||
*/
|
||||
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.
|
||||
* Interface for wind generators. It allows for generating a baseline wind, and subsequent winds.
|
||||
*/
|
||||
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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package network.MessageControllers;
|
||||
|
||||
|
||||
|
||||
public class MessageController {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,359 @@
|
||||
package shared.model;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import network.Messages.Enums.RaceStatusEnum;
|
||||
import network.Messages.Enums.RaceTypeEnum;
|
||||
import shared.dataInput.BoatDataSource;
|
||||
import shared.dataInput.RaceDataSource;
|
||||
import shared.dataInput.RegattaDataSource;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a yacht race.
|
||||
* This is a base class inherited by {@link mock.model.MockRace} and {@link visualiser.model.VisualiserRaceState}.
|
||||
* Has a course, state, wind, boundaries, etc.... Boats are added by inheriting classes (see {@link Boat}, {@link mock.model.MockBoat}, {@link visualiser.model.VisualiserBoat}.
|
||||
*/
|
||||
public abstract class RaceState {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Data source for race information.
|
||||
*/
|
||||
private RaceDataSource raceDataSource;
|
||||
|
||||
/**
|
||||
* Data source for boat information.
|
||||
*/
|
||||
private BoatDataSource boatDataSource;
|
||||
|
||||
/**
|
||||
* Data source for regatta information.
|
||||
*/
|
||||
private RegattaDataSource regattaDataSource;
|
||||
|
||||
/**
|
||||
* Legs in the race.
|
||||
* We have this in a separate list so that it can be observed.
|
||||
*/
|
||||
private ObservableList<Leg> legs;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The clock which tracks the race's start time, current time, and elapsed duration.
|
||||
*/
|
||||
private RaceClock raceClock;
|
||||
|
||||
|
||||
/**
|
||||
* The current status of the race.
|
||||
*/
|
||||
private RaceStatusEnum raceStatusEnum;
|
||||
|
||||
|
||||
/**
|
||||
* The race's wind.
|
||||
*/
|
||||
private Property<Wind> raceWind = new SimpleObjectProperty<>();
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructs an empty race object.
|
||||
* This is initialised into a "default" state, with no data.
|
||||
*/
|
||||
public RaceState() {
|
||||
|
||||
//Legs.
|
||||
this.legs = FXCollections.observableArrayList();
|
||||
|
||||
//Race clock.
|
||||
this.raceClock = new RaceClock(ZonedDateTime.now());
|
||||
|
||||
//Race status.
|
||||
this.setRaceStatusEnum(RaceStatusEnum.NOT_ACTIVE);
|
||||
|
||||
//Wind.
|
||||
this.setWind(Bearing.fromDegrees(0), 0);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Initialise the boats in the race.
|
||||
* This sets their starting positions and current legs.
|
||||
*/
|
||||
protected abstract void initialiseBoats();
|
||||
|
||||
|
||||
/**
|
||||
* Updates the race to use a new list of legs, and adds a dummy "Finish" leg at the end.
|
||||
* @param legs The new list of legs to use.
|
||||
*/
|
||||
protected void useLegsList(List<Leg> legs) {
|
||||
this.legs.setAll(legs);
|
||||
//We add a "dummy" leg at the end of the race.
|
||||
if (getLegs().size() > 0) {
|
||||
getLegs().add(new Leg("Finish", getLegs().size()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether or not a specific leg is the last leg in the race.
|
||||
* @param leg The leg to check.
|
||||
* @return Returns true if it is the last, false otherwise.
|
||||
*/
|
||||
protected boolean isLastLeg(Leg leg) {
|
||||
|
||||
//Get the last leg.
|
||||
Leg lastLeg = getLegs().get(getLegs().size() - 1);
|
||||
|
||||
//Check its ID.
|
||||
int lastLegID = lastLeg.getLegNumber();
|
||||
|
||||
//Get the specified leg's ID.
|
||||
int legID = leg.getLegNumber();
|
||||
|
||||
|
||||
//Check if they are the same.
|
||||
return legID == lastLegID;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the race data source for the race.
|
||||
* @param raceDataSource New race data source.
|
||||
*/
|
||||
public void setRaceDataSource(RaceDataSource raceDataSource) {
|
||||
this.raceDataSource = raceDataSource;
|
||||
this.getRaceClock().setStartingTime(raceDataSource.getStartDateTime());
|
||||
useLegsList(raceDataSource.getLegs());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the boat data source for the race.
|
||||
* @param boatDataSource New boat data source.
|
||||
*/
|
||||
public void setBoatDataSource(BoatDataSource boatDataSource) {
|
||||
this.boatDataSource = boatDataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the regatta data source for the race.
|
||||
* @param regattaDataSource New regatta data source.
|
||||
*/
|
||||
public void setRegattaDataSource(RegattaDataSource regattaDataSource) {
|
||||
this.regattaDataSource = regattaDataSource;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the race data source for the race.
|
||||
* @return Race data source.
|
||||
*/
|
||||
public RaceDataSource getRaceDataSource() {
|
||||
return raceDataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the race data source for the race.
|
||||
* @return Race data source.
|
||||
*/
|
||||
public BoatDataSource getBoatDataSource() {
|
||||
return boatDataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the race data source for the race.
|
||||
* @return Race data source.
|
||||
*/
|
||||
public RegattaDataSource getRegattaDataSource() {
|
||||
return regattaDataSource;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of {@link Mark} boats.
|
||||
* @return List of mark boats.
|
||||
*/
|
||||
public List<Mark> getMarks() {
|
||||
return new ArrayList<>(boatDataSource.getMarkerBoats().values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of sourceIDs participating in the race.
|
||||
* @return List of sourceIDs participating in the race.
|
||||
*/
|
||||
public List<Integer> getParticipants() {
|
||||
return raceDataSource.getParticipants();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the current race status.
|
||||
* @return The current race status.
|
||||
*/
|
||||
public RaceStatusEnum getRaceStatusEnum() {
|
||||
return raceStatusEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current race status.
|
||||
* @param raceStatusEnum The new status of the race.
|
||||
*/
|
||||
public void setRaceStatusEnum(RaceStatusEnum raceStatusEnum) {
|
||||
this.raceStatusEnum = raceStatusEnum;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the type of race this is.
|
||||
* @return The type of race this is.
|
||||
*/
|
||||
public RaceTypeEnum getRaceType() {
|
||||
return raceDataSource.getRaceType();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the name of the regatta.
|
||||
* @return The name of the regatta.
|
||||
*/
|
||||
public String getRegattaName() {
|
||||
return regattaDataSource.getRegattaName();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the race to have a specified wind bearing and speed.
|
||||
* @param windBearing New wind bearing.
|
||||
* @param windSpeedKnots New wind speed, in knots.
|
||||
*/
|
||||
public void setWind(Bearing windBearing, double windSpeedKnots) {
|
||||
Wind wind = new Wind(windBearing, windSpeedKnots);
|
||||
setWind(wind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the race to have a specified wind (bearing and speed).
|
||||
* @param wind New wind.
|
||||
*/
|
||||
public void setWind(Wind wind) {
|
||||
this.raceWind.setValue(wind);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the wind bearing.
|
||||
* @return The wind bearing.
|
||||
*/
|
||||
public Bearing getWindDirection() {
|
||||
return raceWind.getValue().getWindDirection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wind speed.
|
||||
* Measured in knots.
|
||||
* @return The wind speed.
|
||||
*/
|
||||
public double getWindSpeed() {
|
||||
return raceWind.getValue().getWindSpeed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the race's wind.
|
||||
* @return The race's wind.
|
||||
*/
|
||||
public Property<Wind> windProperty() {
|
||||
return raceWind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RaceClock for this race.
|
||||
* This is used to track the start time, current time, and elapsed duration of the race.
|
||||
* @return The RaceClock for the race.
|
||||
*/
|
||||
public RaceClock getRaceClock() {
|
||||
return raceClock;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the number of legs in the race.
|
||||
* @return The number of legs in the race.
|
||||
*/
|
||||
public int getLegCount() {
|
||||
//We minus one, as we have added an extra "dummy" leg.
|
||||
return getLegs().size() - 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the race boundary.
|
||||
* @return The race boundary.
|
||||
*/
|
||||
public List<GPSCoordinate> getBoundary() {
|
||||
return raceDataSource.getBoundary();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the marks of the race.
|
||||
* @return Marks of the race.
|
||||
*/
|
||||
public List<CompoundMark> getCompoundMarks() {
|
||||
return raceDataSource.getCompoundMarks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the legs of the race.
|
||||
* @return Legs of the race.
|
||||
*/
|
||||
public ObservableList<Leg> getLegs() {
|
||||
return legs;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the ID of the race.
|
||||
* @return ID of the race.
|
||||
*/
|
||||
public int getRaceId() {
|
||||
return raceDataSource.getRaceId();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the ID of the regatta.
|
||||
* @return The ID of the regatta.
|
||||
*/
|
||||
public int getRegattaID() {
|
||||
return regattaDataSource.getRegattaID();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the name of the course.
|
||||
* @return Name of the course.
|
||||
*/
|
||||
public String getCourseName() {
|
||||
return regattaDataSource.getCourseName();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package shared.model;
|
||||
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* This interface is a {@link Runnable} interface, with the ability to sleep until a given time period has elapsed.
|
||||
*/
|
||||
public interface RunnableWithFramePeriod extends Runnable {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Waits for enough time for the period of this frame to be greater than minimumFramePeriod.
|
||||
* @param previousFrameTime The timestamp of the previous frame.
|
||||
* @param currentFrameTime The timestamp of the current frame.
|
||||
* @param minimumFramePeriod The minimum period the frame must be.
|
||||
*/
|
||||
default void waitForFramePeriod(long previousFrameTime, long currentFrameTime, long minimumFramePeriod) {
|
||||
|
||||
|
||||
//This is the time elapsed, in milliseconds, since the last server "frame".
|
||||
long framePeriod = currentFrameTime - previousFrameTime;
|
||||
|
||||
//We only attempt to send packets every X milliseconds.
|
||||
if (framePeriod >= minimumFramePeriod) {
|
||||
return;
|
||||
|
||||
} else {
|
||||
//Wait until the frame period will be large enough.
|
||||
long timeToWait = minimumFramePeriod - framePeriod;
|
||||
|
||||
try {
|
||||
Thread.sleep(timeToWait);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
//If we get interrupted, exit the function.
|
||||
Logger.getGlobal().log(Level.SEVERE, "RunnableWithFramePeriod.waitForFramePeriod().sleep(framePeriod) was interrupted on thread: " + Thread.currentThread(), e);
|
||||
//Re-set the interrupt flag.
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package visualiser.Commands.ConnectionToServerCommands;
|
||||
|
||||
|
||||
import mock.exceptions.CommandConstructionException;
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.JoinAcceptance;
|
||||
import visualiser.network.ConnectionToServer;
|
||||
|
||||
/**
|
||||
* Factory to create ConnectionToServer commands.
|
||||
*/
|
||||
public class ConnectionToServerCommandFactory {
|
||||
|
||||
/**
|
||||
* Generates a command to execute on server connection based on the type of {@link network.Messages.Enums.JoinAcceptanceEnum}.
|
||||
* @param message The message to turn into a command.
|
||||
* @param connectionToServer The connection for the command to operate on.
|
||||
* @return The command to execute the given action.
|
||||
* @throws CommandConstructionException Thrown if the command cannot be constructed.
|
||||
*/
|
||||
public static Command create(AC35Data message, ConnectionToServer connectionToServer) throws CommandConstructionException {
|
||||
|
||||
if (!(message instanceof JoinAcceptance)) {
|
||||
throw new CommandConstructionException("Message: " + message + " is not a JoinAcceptance message.");
|
||||
}
|
||||
|
||||
JoinAcceptance joinAcceptance = (JoinAcceptance) message;
|
||||
|
||||
|
||||
switch(joinAcceptance.getAcceptanceType()) {
|
||||
|
||||
case JOIN_SUCCESSFUL_PARTICIPANT: return new JoinSuccessParticipantCommand(joinAcceptance, connectionToServer);
|
||||
|
||||
case JOIN_SUCCESSFUL_SPECTATOR: return new JoinSuccessSpectatorCommand(joinAcceptance, connectionToServer);
|
||||
|
||||
case JOIN_FAILURE: return new JoinFailureCommand(joinAcceptance, connectionToServer);
|
||||
|
||||
case SERVER_FULL: return new ServerFullCommand(joinAcceptance, connectionToServer);
|
||||
|
||||
default: throw new CommandConstructionException("Could not create command for JoinAcceptance: " + joinAcceptance + ". Unknown JoinAcceptanceEnum.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package visualiser.Commands.ConnectionToServerCommands;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.JoinAcceptance;
|
||||
import visualiser.enums.ConnectionToServerState;
|
||||
import visualiser.network.ConnectionToServer;
|
||||
|
||||
|
||||
/**
|
||||
* Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_FAILURE} {@link JoinAcceptance} message is received.
|
||||
*/
|
||||
public class JoinFailureCommand implements Command {
|
||||
|
||||
/**
|
||||
* The message to operate on.
|
||||
*/
|
||||
private JoinAcceptance joinAcceptance;
|
||||
|
||||
/**
|
||||
* The context to operate on.
|
||||
*/
|
||||
private ConnectionToServer connectionToServer;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link JoinFailureCommand}, which operates on a given {@link ConnectionToServer}.
|
||||
* @param joinAcceptance The message to operate on.
|
||||
* @param connectionToServer The context to operate on.
|
||||
*/
|
||||
public JoinFailureCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
|
||||
this.joinAcceptance = joinAcceptance;
|
||||
this.connectionToServer = connectionToServer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
connectionToServer.setJoinAcceptance(joinAcceptance);
|
||||
|
||||
connectionToServer.setConnectionState(ConnectionToServerState.DECLINED);
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package visualiser.Commands.ConnectionToServerCommands;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.AssignPlayerBoat;
|
||||
import network.Messages.JoinAcceptance;
|
||||
import visualiser.enums.ConnectionToServerState;
|
||||
import visualiser.network.ConnectionToServer;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_SUCCESSFUL_PARTICIPANT} {@link network.Messages.JoinAcceptance} message is received.
|
||||
*/
|
||||
public class JoinSuccessParticipantCommand implements Command {
|
||||
|
||||
/**
|
||||
* The message to operate on.
|
||||
*/
|
||||
private JoinAcceptance joinAcceptance;
|
||||
|
||||
/**
|
||||
* The context to operate on.
|
||||
*/
|
||||
private ConnectionToServer connectionToServer;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link JoinSuccessParticipantCommand}, which operates on a given {@link ConnectionToServer}.
|
||||
* @param joinAcceptance The message to operate on.
|
||||
* @param connectionToServer The context to operate on.
|
||||
*/
|
||||
public JoinSuccessParticipantCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
|
||||
this.joinAcceptance = joinAcceptance;
|
||||
this.connectionToServer = connectionToServer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
connectionToServer.setJoinAcceptance(joinAcceptance);
|
||||
|
||||
connectionToServer.setConnectionState(ConnectionToServerState.CONNECTED);
|
||||
|
||||
|
||||
AssignPlayerBoat assignPlayerBoat = new AssignPlayerBoat(joinAcceptance.getSourceID());
|
||||
try {
|
||||
connectionToServer.send(assignPlayerBoat);
|
||||
} catch (InterruptedException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "JoinSuccessParticipantCommand: " + this + " was interrupted on thread: " + Thread.currentThread() + " while sending AssignPlayerBoat message.", e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
package visualiser.Commands.ConnectionToServerCommands;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.JoinAcceptance;
|
||||
import visualiser.enums.ConnectionToServerState;
|
||||
import visualiser.network.ConnectionToServer;
|
||||
|
||||
|
||||
/**
|
||||
* Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_SUCCESSFUL_PARTICIPANT} {@link JoinAcceptance} message is received.
|
||||
*/
|
||||
public class JoinSuccessSpectatorCommand implements Command {
|
||||
|
||||
/**
|
||||
* The message to operate on.
|
||||
*/
|
||||
private JoinAcceptance joinAcceptance;
|
||||
|
||||
/**
|
||||
* The context to operate on.
|
||||
*/
|
||||
private ConnectionToServer connectionToServer;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link JoinSuccessSpectatorCommand}, which operates on a given {@link ConnectionToServer}.
|
||||
* @param joinAcceptance The message to operate on.
|
||||
* @param connectionToServer The context to operate on.
|
||||
*/
|
||||
public JoinSuccessSpectatorCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
|
||||
this.joinAcceptance = joinAcceptance;
|
||||
this.connectionToServer = connectionToServer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
connectionToServer.setJoinAcceptance(joinAcceptance);
|
||||
|
||||
connectionToServer.setConnectionState(ConnectionToServerState.CONNECTED);
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package visualiser.Commands.ConnectionToServerCommands;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.JoinAcceptance;
|
||||
import visualiser.enums.ConnectionToServerState;
|
||||
import visualiser.network.ConnectionToServer;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
/**
|
||||
* Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#SERVER_FULL} {@link JoinAcceptance} message is received.
|
||||
*/
|
||||
public class ServerFullCommand implements Command {
|
||||
|
||||
/**
|
||||
* The message to operate on.
|
||||
*/
|
||||
private JoinAcceptance joinAcceptance;
|
||||
|
||||
/**
|
||||
* The context to operate on.
|
||||
*/
|
||||
private ConnectionToServer connectionToServer;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link ServerFullCommand}, which operates on a given {@link ConnectionToServer}.
|
||||
* @param joinAcceptance The message to operate on.
|
||||
* @param connectionToServer The context to operate on.
|
||||
*/
|
||||
public ServerFullCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
|
||||
this.joinAcceptance = joinAcceptance;
|
||||
this.connectionToServer = connectionToServer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
connectionToServer.setJoinAcceptance(joinAcceptance);
|
||||
|
||||
connectionToServer.setConnectionState(ConnectionToServerState.DECLINED);
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package visualiser.Commands.IncomingHeartBeatCommands;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.HeartBeat;
|
||||
import network.Messages.JoinAcceptance;
|
||||
import visualiser.enums.ConnectionToServerState;
|
||||
import visualiser.network.ConnectionToServer;
|
||||
import visualiser.network.IncomingHeartBeatService;
|
||||
|
||||
|
||||
/**
|
||||
* Command created when a {@link HeartBeat} message is received.
|
||||
*/
|
||||
public class IncomingHeartBeatCommand implements Command {
|
||||
|
||||
/**
|
||||
* The message to operate on.
|
||||
*/
|
||||
private HeartBeat heartBeat;
|
||||
|
||||
/**
|
||||
* The context to operate on.
|
||||
*/
|
||||
private IncomingHeartBeatService incomingHeartBeatService;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link IncomingHeartBeatCommand}, which operates on a given {@link IncomingHeartBeatService}.
|
||||
* @param heartBeat The message to operate on.
|
||||
* @param incomingHeartBeatService The context to operate on.
|
||||
*/
|
||||
public IncomingHeartBeatCommand(HeartBeat heartBeat, IncomingHeartBeatService incomingHeartBeatService) {
|
||||
this.heartBeat = heartBeat;
|
||||
this.incomingHeartBeatService = incomingHeartBeatService;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
incomingHeartBeatService.setLastHeartBeatSeqNum(heartBeat.getSequenceNumber());
|
||||
|
||||
incomingHeartBeatService.setLastHeartbeatTime(System.currentTimeMillis());
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package visualiser.Commands.IncomingHeartBeatCommands;
|
||||
|
||||
|
||||
import mock.exceptions.CommandConstructionException;
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.HeartBeat;
|
||||
import visualiser.network.IncomingHeartBeatService;
|
||||
|
||||
/**
|
||||
* Factory to create IncomingHeartBeatService commands.
|
||||
*/
|
||||
public class IncomingHeartBeatCommandFactory {
|
||||
|
||||
/**
|
||||
* Generates a command on an IncomingHeartBeatService.
|
||||
* @param message The message to turn into a command.
|
||||
* @param incomingHeartBeatService The context for the command to operate on.
|
||||
* @return The command to execute the given action.
|
||||
* @throws CommandConstructionException Thrown if the command cannot be constructed.
|
||||
*/
|
||||
public static Command create(AC35Data message, IncomingHeartBeatService incomingHeartBeatService) throws CommandConstructionException {
|
||||
|
||||
if (!(message instanceof HeartBeat)) {
|
||||
throw new CommandConstructionException("Message: " + message + " is not a HeartBeat message.");
|
||||
}
|
||||
|
||||
HeartBeat heartBeat = (HeartBeat) message;
|
||||
|
||||
return new IncomingHeartBeatCommand(heartBeat, incomingHeartBeatService);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package visualiser.Commands.VisualiserRaceCommands;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.AssignPlayerBoat;
|
||||
import network.Messages.BoatLocation;
|
||||
import shared.exceptions.BoatNotFoundException;
|
||||
import shared.exceptions.MarkNotFoundException;
|
||||
import shared.model.GPSCoordinate;
|
||||
import shared.model.Mark;
|
||||
import visualiser.model.VisualiserBoat;
|
||||
import visualiser.model.VisualiserRaceState;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* Command created when a {@link AssignPlayerBoat} message is received.
|
||||
*/
|
||||
public class AssignPlayerBoatCommand implements Command {
|
||||
|
||||
/**
|
||||
* The message to operate on.
|
||||
*/
|
||||
private AssignPlayerBoat assignPlayerBoat;
|
||||
|
||||
/**
|
||||
* The context to operate on.
|
||||
*/
|
||||
private VisualiserRaceState visualiserRace;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link AssignPlayerBoatCommand}, which operates on a given {@link VisualiserRaceState}.
|
||||
* @param assignPlayerBoat The message to operate on.
|
||||
* @param visualiserRace The context to operate on.
|
||||
*/
|
||||
public AssignPlayerBoatCommand(AssignPlayerBoat assignPlayerBoat, VisualiserRaceState visualiserRace) {
|
||||
this.assignPlayerBoat = assignPlayerBoat;
|
||||
this.visualiserRace = visualiserRace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
visualiserRace.setPlayerBoatID(assignPlayerBoat.getSourceID());
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
package visualiser.Commands.VisualiserRaceCommands;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.BoatLocation;
|
||||
import network.Messages.Enums.BoatStatusEnum;
|
||||
import shared.exceptions.BoatNotFoundException;
|
||||
import shared.exceptions.MarkNotFoundException;
|
||||
import shared.model.GPSCoordinate;
|
||||
import shared.model.Mark;
|
||||
import visualiser.model.VisualiserBoat;
|
||||
import visualiser.model.VisualiserRaceEvent;
|
||||
import visualiser.model.VisualiserRaceState;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* Command created when a {@link BoatLocation} message is received.
|
||||
*/
|
||||
public class BoatLocationCommand implements Command {
|
||||
|
||||
/**
|
||||
* The message to operate on.
|
||||
*/
|
||||
private BoatLocation boatLocation;
|
||||
|
||||
/**
|
||||
* The context to operate on.
|
||||
*/
|
||||
private VisualiserRaceState visualiserRace;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link BoatLocationCommand}, which operates on a given {@link VisualiserRaceState}.
|
||||
* @param boatLocation The message to operate on.
|
||||
* @param visualiserRace The context to operate on.
|
||||
*/
|
||||
public BoatLocationCommand(BoatLocation boatLocation, VisualiserRaceState visualiserRace) {
|
||||
this.boatLocation = boatLocation;
|
||||
this.visualiserRace = visualiserRace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
if (visualiserRace.isVisualiserBoat(boatLocation.getSourceID())) {
|
||||
updateBoatLocation();
|
||||
} else if (visualiserRace.isMark(boatLocation.getSourceID())) {
|
||||
updateMarkLocation();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the boat specified in the message.
|
||||
*/
|
||||
private void updateBoatLocation() {
|
||||
|
||||
try {
|
||||
VisualiserBoat boat = visualiserRace.getBoat(boatLocation.getSourceID());
|
||||
|
||||
//Get the new position.
|
||||
GPSCoordinate gpsCoordinate = new GPSCoordinate(
|
||||
boatLocation.getLatitude(),
|
||||
boatLocation.getLongitude());
|
||||
|
||||
boat.setCurrentPosition(gpsCoordinate);
|
||||
|
||||
//Bearing.
|
||||
boat.setBearing(boatLocation.getHeading());
|
||||
|
||||
|
||||
//Speed.
|
||||
boat.setCurrentSpeed(boatLocation.getBoatSpeedKnots());
|
||||
|
||||
|
||||
//Attempt to add a track point.
|
||||
attemptAddTrackPoint(boat);
|
||||
|
||||
|
||||
} catch (BoatNotFoundException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "BoatLocationCommand: " + this + " could not execute. Boat with sourceID: " + boatLocation.getSourceID() + " not found.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempts to add a track point to the boat. Only works if the boat is currently racing.
|
||||
* @param boat The boat to add a track point to.
|
||||
*/
|
||||
private void attemptAddTrackPoint(VisualiserBoat boat) {
|
||||
if (boat.getStatus() == BoatStatusEnum.RACING) {
|
||||
boat.addTrackPoint(boat.getCurrentPosition(), visualiserRace.getRaceClock().getCurrentTime());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the marker boat specified in message.
|
||||
*/
|
||||
private void updateMarkLocation() {
|
||||
|
||||
try {
|
||||
Mark mark = visualiserRace.getMark(boatLocation.getSourceID());
|
||||
|
||||
GPSCoordinate gpsCoordinate = new GPSCoordinate(
|
||||
boatLocation.getLatitude(),
|
||||
boatLocation.getLongitude());
|
||||
|
||||
mark.setPosition(gpsCoordinate);
|
||||
} catch (MarkNotFoundException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "BoatLocationCommand: " + this + " could not execute. Mark with sourceID: " + boatLocation.getSourceID() + " not found.", e);
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package visualiser.Commands.VisualiserRaceCommands;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.XMLMessage;
|
||||
import shared.dataInput.BoatDataSource;
|
||||
import shared.dataInput.BoatXMLReader;
|
||||
import shared.enums.XMLFileType;
|
||||
import shared.exceptions.InvalidBoatDataException;
|
||||
import shared.exceptions.XMLReaderException;
|
||||
import visualiser.model.VisualiserRaceEvent;
|
||||
import visualiser.model.VisualiserRaceState;
|
||||
|
||||
|
||||
/**
|
||||
* Command created when a {@link network.Messages.Enums.XMLMessageType#BOAT} {@link XMLMessage} message is received.
|
||||
*/
|
||||
public class BoatsXMLMessageCommand implements Command {
|
||||
|
||||
/**
|
||||
* The data source to operate on.
|
||||
*/
|
||||
private BoatDataSource boatDataSource;
|
||||
|
||||
/**
|
||||
* The context to operate on.
|
||||
*/
|
||||
private VisualiserRaceState visualiserRace;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link BoatsXMLMessageCommand}, which operates on a given {@link VisualiserRaceEvent}.
|
||||
* @param boatDataSource The data source to operate on.
|
||||
* @param visualiserRace The context to operate on.
|
||||
*/
|
||||
public BoatsXMLMessageCommand(BoatDataSource boatDataSource, VisualiserRaceState visualiserRace) {
|
||||
this.boatDataSource = boatDataSource;
|
||||
this.visualiserRace = visualiserRace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
visualiserRace.setBoatDataSource(boatDataSource);
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,185 @@
|
||||
package visualiser.Commands.VisualiserRaceCommands;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.BoatStatus;
|
||||
import network.Messages.Enums.BoatStatusEnum;
|
||||
import network.Messages.RaceStatus;
|
||||
import shared.exceptions.BoatNotFoundException;
|
||||
import shared.model.Leg;
|
||||
import visualiser.model.VisualiserBoat;
|
||||
import visualiser.model.VisualiserRaceEvent;
|
||||
import visualiser.model.VisualiserRaceState;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* Command created when a {@link RaceStatus} message is received.
|
||||
*/
|
||||
public class RaceStatusCommand implements Command {
|
||||
|
||||
/**
|
||||
* The message to operate on.
|
||||
*/
|
||||
private RaceStatus raceStatus;
|
||||
|
||||
/**
|
||||
* The context to operate on.
|
||||
*/
|
||||
private VisualiserRaceState visualiserRace;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link RaceStatusCommand}, which operates on a given {@link VisualiserRaceState}.
|
||||
* @param raceStatus The message to operate on.
|
||||
* @param visualiserRace The context to operate on.
|
||||
*/
|
||||
public RaceStatusCommand(RaceStatus raceStatus, VisualiserRaceState visualiserRace) {
|
||||
this.raceStatus = raceStatus;
|
||||
this.visualiserRace = visualiserRace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
//Race status enum.
|
||||
visualiserRace.setRaceStatusEnum(raceStatus.getRaceStatus());
|
||||
|
||||
//Wind.
|
||||
visualiserRace.setWind(
|
||||
raceStatus.getWindDirection(),
|
||||
raceStatus.getWindSpeed() );
|
||||
|
||||
//Current race time.
|
||||
visualiserRace.getRaceClock().setUTCTime(raceStatus.getCurrentTime());
|
||||
|
||||
|
||||
|
||||
for (BoatStatus boatStatus : raceStatus.getBoatStatuses()) {
|
||||
updateBoatStatus(boatStatus);
|
||||
}
|
||||
|
||||
|
||||
visualiserRace.updateBoatPositions(visualiserRace.getBoats());
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates a single boat's status using the boatStatus message.
|
||||
* @param boatStatus BoatStatus message to get data from.
|
||||
*/
|
||||
private void updateBoatStatus(BoatStatus boatStatus) {
|
||||
try {
|
||||
VisualiserBoat boat = visualiserRace.getBoat(boatStatus.getSourceID());
|
||||
|
||||
//Time at next mark.
|
||||
updateEstimatedTimeAtNextMark(boatStatus, boat);
|
||||
|
||||
|
||||
BoatStatusEnum newBoatStatusEnum = boatStatus.getBoatStatus();
|
||||
|
||||
//Time at last mark.
|
||||
initialiseTimeAtLastMark(boat, boat.getStatus(), newBoatStatusEnum);
|
||||
|
||||
//Status.
|
||||
boat.setStatus(newBoatStatusEnum);
|
||||
|
||||
|
||||
List<Leg> legs = visualiserRace.getLegs();
|
||||
|
||||
//Leg.
|
||||
updateLeg(boatStatus.getLegNumber(), boat, legs);
|
||||
|
||||
|
||||
//Set finish time if boat finished.
|
||||
attemptUpdateFinishTime(boatStatus, boat, legs);
|
||||
|
||||
|
||||
} catch (BoatNotFoundException e) {
|
||||
//Logger.getGlobal().log(Level.WARNING, "RaceStatusCommand.updateBoatStatus: " + this + " could not execute. Boat with sourceID: " + boatStatus.getSourceID() + " not found.", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempts to update the finish time of the boat. Only works if the boat has actually finished the race.
|
||||
* @param boatStatus BoatStatus to read data from.
|
||||
* @param boat Boat to update.
|
||||
* @param legs Legs of the race.
|
||||
*/
|
||||
private void attemptUpdateFinishTime(BoatStatus boatStatus, VisualiserBoat boat, List<Leg> legs) {
|
||||
|
||||
if (boat.getStatus() == BoatStatusEnum.FINISHED || boatStatus.getLegNumber() == legs.size()) {
|
||||
boat.setTimeFinished(visualiserRace.getRaceClock().getCurrentTimeMilli());
|
||||
boat.setStatus(BoatStatusEnum.FINISHED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Updates a boat's leg.
|
||||
* @param legNumber The new leg number.
|
||||
* @param boat The boat to update.
|
||||
* @param legs The legs in the race.
|
||||
*/
|
||||
private void updateLeg(int legNumber, VisualiserBoat boat, List<Leg> legs) {
|
||||
|
||||
if (legNumber >= 1 && legNumber < legs.size()) {
|
||||
if (boat.getCurrentLeg() != legs.get(legNumber)) {
|
||||
boatFinishedLeg(boat, legs.get(legNumber));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialises the time at last mark for a boat. Only changes if the boat's status is changing from non-racing to racing.
|
||||
* @param boat The boat to update.
|
||||
* @param currentBoatStatus The current status of the boat.
|
||||
* @param newBoatStatusEnum The new status of the boat, from the BoatStatus message.
|
||||
*/
|
||||
private void initialiseTimeAtLastMark(VisualiserBoat boat, BoatStatusEnum currentBoatStatus, BoatStatusEnum newBoatStatusEnum) {
|
||||
//If we are changing from non-racing to racing, we need to initialise boat with their time at last mark.
|
||||
if ((currentBoatStatus != BoatStatusEnum.RACING) && (newBoatStatusEnum == BoatStatusEnum.RACING)) {
|
||||
boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the estimated time at next mark for a given boat.
|
||||
* @param boatStatus BoatStatus to read data from.
|
||||
* @param boat Boat to update.
|
||||
*/
|
||||
private void updateEstimatedTimeAtNextMark(BoatStatus boatStatus, VisualiserBoat boat) {
|
||||
boat.setEstimatedTimeAtNextMark(visualiserRace.getRaceClock().getLocalTime(boatStatus.getEstTimeAtNextMark()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Updates a boat's leg to a specified leg. Also records the order in which the boat passed the leg.
|
||||
* @param boat The boat to update.
|
||||
* @param leg The leg to use.
|
||||
*/
|
||||
private void boatFinishedLeg(VisualiserBoat boat, Leg leg) {
|
||||
|
||||
//Record order in which boat finished leg.
|
||||
visualiserRace.getLegCompletionOrder().get(boat.getCurrentLeg()).add(boat);
|
||||
|
||||
//Update boat.
|
||||
boat.setCurrentLeg(leg);
|
||||
boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package visualiser.Commands.VisualiserRaceCommands;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.XMLMessage;
|
||||
import shared.dataInput.RaceDataSource;
|
||||
import visualiser.model.VisualiserRaceEvent;
|
||||
import visualiser.model.VisualiserRaceState;
|
||||
|
||||
|
||||
/**
|
||||
* Command created when a {@link network.Messages.Enums.XMLMessageType#BOAT} {@link XMLMessage} message is received.
|
||||
*/
|
||||
public class RaceXMLMessageCommand implements Command {
|
||||
|
||||
/**
|
||||
* The data source to operate on.
|
||||
*/
|
||||
private RaceDataSource raceDataSource;
|
||||
|
||||
/**
|
||||
* The context to operate on.
|
||||
*/
|
||||
private VisualiserRaceState visualiserRace;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link RaceXMLMessageCommand}, which operates on a given {@link VisualiserRaceEvent}.
|
||||
* @param raceDataSource The data source to operate on.
|
||||
* @param visualiserRace The context to operate on.
|
||||
*/
|
||||
public RaceXMLMessageCommand(RaceDataSource raceDataSource, VisualiserRaceState visualiserRace) {
|
||||
this.raceDataSource = raceDataSource;
|
||||
this.visualiserRace = visualiserRace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
visualiserRace.setRaceDataSource(raceDataSource);
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package visualiser.Commands.VisualiserRaceCommands;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.XMLMessage;
|
||||
import shared.dataInput.RegattaDataSource;
|
||||
import visualiser.model.VisualiserRaceEvent;
|
||||
import visualiser.model.VisualiserRaceState;
|
||||
|
||||
|
||||
/**
|
||||
* Command created when a {@link network.Messages.Enums.XMLMessageType#BOAT} {@link XMLMessage} message is received.
|
||||
*/
|
||||
public class RegattaXMLMessageCommand implements Command {
|
||||
|
||||
/**
|
||||
* The data source to operate on.
|
||||
*/
|
||||
private RegattaDataSource regattaDataSource;
|
||||
|
||||
/**
|
||||
* The context to operate on.
|
||||
*/
|
||||
private VisualiserRaceState visualiserRace;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link RegattaXMLMessageCommand}, which operates on a given {@link VisualiserRaceEvent}.
|
||||
* @param regattaDataSource The data source to operate on.
|
||||
* @param visualiserRace The context to operate on.
|
||||
*/
|
||||
public RegattaXMLMessageCommand(RegattaDataSource regattaDataSource, VisualiserRaceState visualiserRace) {
|
||||
this.regattaDataSource = regattaDataSource;
|
||||
this.visualiserRace = visualiserRace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
|
||||
visualiserRace.setRegattaDataSource(regattaDataSource);
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package visualiser.Commands.VisualiserRaceCommands;
|
||||
|
||||
|
||||
import mock.exceptions.CommandConstructionException;
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.*;
|
||||
import visualiser.model.VisualiserRaceEvent;
|
||||
import visualiser.model.VisualiserRaceState;
|
||||
|
||||
/**
|
||||
* Factory to create VisualiserRace commands.
|
||||
*/
|
||||
public class VisualiserRaceCommandFactory {
|
||||
|
||||
/**
|
||||
* Generates a command on an VisualiserRace.
|
||||
* @param message The message to turn into a command.
|
||||
* @param visualiserRace The context for the command to operate on.
|
||||
* @return The command to execute the given action.
|
||||
* @throws CommandConstructionException Thrown if the command cannot be constructed.
|
||||
*/
|
||||
public static Command create(AC35Data message, VisualiserRaceState visualiserRace) throws CommandConstructionException {
|
||||
|
||||
switch (message.getType()) {
|
||||
|
||||
case BOATLOCATION: return new BoatLocationCommand((BoatLocation) message, visualiserRace);
|
||||
|
||||
case RACESTATUS: return new RaceStatusCommand((RaceStatus) message, visualiserRace);
|
||||
|
||||
case XMLMESSAGE: return XMLMessageCommandFactory.create((XMLMessage) message, visualiserRace);
|
||||
|
||||
case ASSIGN_PLAYER_BOAT: return new AssignPlayerBoatCommand((AssignPlayerBoat) message, visualiserRace);
|
||||
|
||||
default: throw new CommandConstructionException("Could not create VisualiserRaceCommand. Unrecognised or unsupported MessageType: " + message.getType());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,63 @@
|
||||
package visualiser.Commands.VisualiserRaceCommands;
|
||||
|
||||
|
||||
import mock.exceptions.CommandConstructionException;
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.BoatLocation;
|
||||
import network.Messages.RaceStatus;
|
||||
import network.Messages.XMLMessage;
|
||||
import shared.dataInput.*;
|
||||
import shared.enums.XMLFileType;
|
||||
import shared.exceptions.InvalidBoatDataException;
|
||||
import shared.exceptions.InvalidRaceDataException;
|
||||
import shared.exceptions.InvalidRegattaDataException;
|
||||
import shared.exceptions.XMLReaderException;
|
||||
import visualiser.model.VisualiserRaceState;
|
||||
|
||||
/**
|
||||
* Factory to create VisualiserRace commands, from XMLMessages.
|
||||
*/
|
||||
public class XMLMessageCommandFactory {
|
||||
|
||||
/**
|
||||
* Generates a command on an VisualiserRace.
|
||||
* @param message The message to turn into a command.
|
||||
* @param visualiserRace The context for the command to operate on.
|
||||
* @return The command to execute the given action.
|
||||
* @throws CommandConstructionException Thrown if the command cannot be constructed.
|
||||
*/
|
||||
public static Command create(XMLMessage message, VisualiserRaceState visualiserRace) throws CommandConstructionException {
|
||||
|
||||
try {
|
||||
|
||||
switch (message.getXmlMsgSubType()) {
|
||||
|
||||
case BOAT:
|
||||
BoatDataSource boatDataSource = new BoatXMLReader(message.getXmlMessage(), XMLFileType.Contents);
|
||||
return new BoatsXMLMessageCommand(boatDataSource, visualiserRace);
|
||||
|
||||
|
||||
case RACE:
|
||||
RaceDataSource raceDataSource = new RaceXMLReader(message.getXmlMessage(), XMLFileType.Contents);
|
||||
return new RaceXMLMessageCommand(raceDataSource, visualiserRace);
|
||||
|
||||
|
||||
case REGATTA:
|
||||
RegattaDataSource regattaDataSource = new RegattaXMLReader(message.getXmlMessage(), XMLFileType.Contents);
|
||||
return new RegattaXMLMessageCommand(regattaDataSource, visualiserRace);
|
||||
|
||||
|
||||
default:
|
||||
throw new CommandConstructionException("Could not create VisualiserRaceCommand/XMLCommand. Unrecognised or unsupported MessageType: " + message.getType());
|
||||
|
||||
}
|
||||
|
||||
} catch (XMLReaderException | InvalidBoatDataException | InvalidRegattaDataException | InvalidRaceDataException e) {
|
||||
throw new CommandConstructionException("Could not create VisualiserRaceCommand/XMLCommand. Could not parse XML message payload.", e);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,363 +0,0 @@
|
||||
package visualiser.app;
|
||||
import network.BinaryMessageDecoder;
|
||||
import network.Exceptions.InvalidMessageException;
|
||||
import network.Messages.*;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static network.Utils.ByteConverter.bytesToShort;
|
||||
|
||||
/**
|
||||
* TCP client which receives packets/messages from a race data source
|
||||
* (e.g., mock source, official source), and exposes them to any observers.
|
||||
*/
|
||||
public class VisualiserInput implements Runnable {
|
||||
|
||||
/**
|
||||
* Timestamp of the last heartbeat.
|
||||
*/
|
||||
private long lastHeartbeatTime = -1;
|
||||
/**
|
||||
* Sequence number of the last heartbeat.
|
||||
*/
|
||||
private long lastHeartbeatSequenceNum = -1;
|
||||
|
||||
|
||||
/**
|
||||
* The socket that we have connected to.
|
||||
*/
|
||||
private Socket connectionSocket;
|
||||
|
||||
|
||||
/**
|
||||
* InputStream (from the socket).
|
||||
*/
|
||||
private DataInputStream inStream;
|
||||
|
||||
|
||||
/**
|
||||
* An object containing the set of latest messages to write to.
|
||||
* Every server frame, VisualiserInput reads messages from its inputStream, and write them to this.
|
||||
*/
|
||||
private LatestMessages latestMessages;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Ctor.
|
||||
* @param socket Socket from which we will receive race data.
|
||||
* @throws IOException If there is something wrong with the socket's input stream.
|
||||
*/
|
||||
public VisualiserInput(Socket socket) throws IOException {
|
||||
|
||||
this.connectionSocket = socket;
|
||||
|
||||
//We wrap a DataInputStream around the socket's InputStream because it has the stream.readFully(buffer) function, which is a blocking read until the buffer has been filled.
|
||||
this.inStream = new DataInputStream(connectionSocket.getInputStream());
|
||||
|
||||
this.latestMessages = new LatestMessages();
|
||||
|
||||
|
||||
this.lastHeartbeatTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the LatestMessages object, which can be queried for any received race related messages.
|
||||
* @return The LatestMessages object.
|
||||
*/
|
||||
public LatestMessages getLatestMessages() {
|
||||
return latestMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the time since last heartbeat, in milliseconds.
|
||||
* @return Time since last heartbeat, in milliseconds..
|
||||
*/
|
||||
private double timeSinceHeartbeat() {
|
||||
long now = System.currentTimeMillis();
|
||||
return (now - lastHeartbeatTime);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reads and returns the next message as an array of bytes from the socket. Use getNextMessage() to get the actual message object instead.
|
||||
* @return Encoded binary message bytes.
|
||||
* @throws IOException Thrown when an error occurs while reading from the socket.
|
||||
*/
|
||||
private byte[] getNextMessageBytes() throws IOException {
|
||||
inStream.mark(0);
|
||||
short CRCLength = 4;
|
||||
short headerLength = 15;
|
||||
|
||||
//Read the header of the next message.
|
||||
byte[] headerBytes = new byte[headerLength];
|
||||
inStream.readFully(headerBytes);
|
||||
|
||||
//Read the message body length.
|
||||
byte[] messageBodyLengthBytes = Arrays.copyOfRange(headerBytes, headerLength - 2, headerLength);
|
||||
short messageBodyLength = bytesToShort(messageBodyLengthBytes);
|
||||
|
||||
//Read the message body.
|
||||
byte[] messageBodyBytes = new byte[messageBodyLength];
|
||||
inStream.readFully(messageBodyBytes);
|
||||
|
||||
//Read the message CRC.
|
||||
byte[] messageCRCBytes = new byte[CRCLength];
|
||||
inStream.readFully(messageCRCBytes);
|
||||
|
||||
//Put the head + body + crc into one large array.
|
||||
ByteBuffer messageBytes = ByteBuffer.allocate(headerBytes.length + messageBodyBytes.length + messageCRCBytes.length);
|
||||
messageBytes.put(headerBytes);
|
||||
messageBytes.put(messageBodyBytes);
|
||||
messageBytes.put(messageCRCBytes);
|
||||
|
||||
return messageBytes.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and returns the next message object from the socket.
|
||||
* @return The message object. Use instanceof for concrete type.
|
||||
* @throws IOException Thrown when an error occurs while reading from the socket.
|
||||
* @throws InvalidMessageException Thrown when the message is invalid in some way.
|
||||
*/
|
||||
private AC35Data getNextMessage() throws IOException, InvalidMessageException
|
||||
{
|
||||
//Get the next message from the socket as a block of bytes.
|
||||
byte[] messageBytes = this.getNextMessageBytes();
|
||||
|
||||
//Decode the binary message into an appropriate message object.
|
||||
BinaryMessageDecoder decoder = new BinaryMessageDecoder(messageBytes);
|
||||
|
||||
return decoder.decode();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Main loop which reads messages from the socket, and exposes them.
|
||||
*/
|
||||
public void run(){
|
||||
boolean receiverLoop = true;
|
||||
//receiver loop that gets the input
|
||||
while (receiverLoop) {
|
||||
|
||||
//If no heartbeat has been received in more the heartbeat period
|
||||
//then the connection will need to be restarted.
|
||||
//System.out.println("time since last heartbeat: " + timeSinceHeartbeat());//TEMP REMOVE
|
||||
long heartBeatPeriod = 10 * 1000;
|
||||
if (timeSinceHeartbeat() > heartBeatPeriod) {
|
||||
System.out.println("Connection has stopped, trying to reconnect.");
|
||||
|
||||
//Attempt to reconnect the socket.
|
||||
try {//This attempt doesn't really work. Under what circumstances would
|
||||
this.connectionSocket = new Socket(this.connectionSocket.getInetAddress(), this.connectionSocket.getPort());
|
||||
//this.connectionSocket.connect(this.connectionSocket.getRemoteSocketAddress());
|
||||
//Reset the heartbeat timer.
|
||||
this.lastHeartbeatTime = System.currentTimeMillis();
|
||||
}
|
||||
catch (IOException e) {
|
||||
System.err.println("Unable to reconnect.");
|
||||
|
||||
//Wait 500ms. Ugly hack, should refactor.
|
||||
long waitPeriod = 500;
|
||||
long waitTimeStart = System.currentTimeMillis() + waitPeriod;
|
||||
|
||||
while (System.currentTimeMillis() < waitTimeStart){
|
||||
//Nothing. Busyloop.
|
||||
}
|
||||
|
||||
//Swallow the exception.
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Reads the next message.
|
||||
AC35Data message;
|
||||
try {
|
||||
message = this.getNextMessage();
|
||||
}
|
||||
catch (InvalidMessageException | IOException e) {
|
||||
//Prints exception to stderr, and iterate loop (that is, read the next message).
|
||||
System.err.println("Unable to read message: " + e.getMessage());
|
||||
try {
|
||||
inStream.reset();
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
//Continue to the next loop iteration/message.
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
//Checks which message is being received and does what is needed for that message.
|
||||
switch (message.getType()) {
|
||||
|
||||
//Heartbeat.
|
||||
case HEARTBEAT: {
|
||||
HeartBeat heartBeat = (HeartBeat) message;
|
||||
|
||||
//Check that the heartbeat number is greater than the previous value, and then set the last heartbeat time.
|
||||
if (heartBeat.getSequenceNumber() > this.lastHeartbeatSequenceNum) {
|
||||
lastHeartbeatTime = System.currentTimeMillis();
|
||||
lastHeartbeatSequenceNum = heartBeat.getSequenceNumber();
|
||||
//System.out.println("HeartBeat Message! " + lastHeartbeatSequenceNum);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//RaceStatus.
|
||||
case RACESTATUS: {
|
||||
RaceStatus raceStatus = (RaceStatus) message;
|
||||
|
||||
//System.out.println("Race Status Message");
|
||||
this.latestMessages.setRaceStatus(raceStatus);
|
||||
|
||||
for (BoatStatus boatStatus : raceStatus.getBoatStatuses()) {
|
||||
this.latestMessages.setBoatStatus(boatStatus);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//DisplayTextMessage.
|
||||
case DISPLAYTEXTMESSAGE: {
|
||||
//System.out.println("Display Text Message");
|
||||
//No decoder for this.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//XMLMessage.
|
||||
case XMLMESSAGE: {
|
||||
XMLMessage xmlMessage = (XMLMessage) message;
|
||||
|
||||
//System.out.println("XML Message!");
|
||||
|
||||
this.latestMessages.setXMLMessage(xmlMessage);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//RaceStartStatus.
|
||||
case RACESTARTSTATUS: {
|
||||
|
||||
//System.out.println("Race Start Status Message");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//YachtEventCode.
|
||||
case YACHTEVENTCODE: {
|
||||
//YachtEventCode yachtEventCode = (YachtEventCode) message;
|
||||
|
||||
//System.out.println("Yacht Event Code!");
|
||||
//No decoder for this.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//YachtActionCode.
|
||||
case YACHTACTIONCODE: {
|
||||
//YachtActionCode yachtActionCode = (YachtActionCode) message;
|
||||
|
||||
//System.out.println("Yacht Action Code!");
|
||||
// No decoder for this.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//ChatterText.
|
||||
case CHATTERTEXT: {
|
||||
//ChatterText chatterText = (ChatterText) message;
|
||||
|
||||
//System.out.println("Chatter Text Message!");
|
||||
//No decoder for this.
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//BoatLocation.
|
||||
case BOATLOCATION: {
|
||||
BoatLocation boatLocation = (BoatLocation) message;
|
||||
|
||||
//System.out.println("Boat Location!");
|
||||
|
||||
BoatLocation existingBoatLocation = this.latestMessages.getBoatLocationMap().get(boatLocation.getSourceID());
|
||||
if (existingBoatLocation != null) {
|
||||
//If our boatlocation map already contains a boat location message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
|
||||
if (boatLocation.getTime() > existingBoatLocation.getTime()) {
|
||||
//If it is, replace the old message.
|
||||
this.latestMessages.setBoatLocation(boatLocation);
|
||||
}
|
||||
} else {
|
||||
//If the map _doesn't_ already contain a message for this boat, insert the message.
|
||||
this.latestMessages.setBoatLocation(boatLocation);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//MarkRounding.
|
||||
case MARKROUNDING: {
|
||||
MarkRounding markRounding = (MarkRounding) message;
|
||||
|
||||
//System.out.println("Mark Rounding Message!");
|
||||
|
||||
MarkRounding existingMarkRounding = this.latestMessages.getMarkRoundingMap().get(markRounding.getSourceID());
|
||||
if (existingMarkRounding != null) {
|
||||
|
||||
//If our markRoundingMap already contains a mark rounding message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
|
||||
if (markRounding.getTime() > existingMarkRounding.getTime()) {
|
||||
//If it is, replace the old message.
|
||||
this.latestMessages.setMarkRounding(markRounding);
|
||||
}
|
||||
|
||||
} else {
|
||||
//If the map _doesn't_ already contain a message for this boat, insert the message.
|
||||
this.latestMessages.setMarkRounding(markRounding);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//CourseWinds.
|
||||
case COURSEWIND: {
|
||||
|
||||
//System.out.println("Course Wind Message!");
|
||||
CourseWinds courseWinds = (CourseWinds) message;
|
||||
|
||||
this.latestMessages.setCourseWinds(courseWinds);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//AverageWind.
|
||||
case AVGWIND: {
|
||||
|
||||
//System.out.println("Average Wind Message!");
|
||||
AverageWind averageWind = (AverageWind) message;
|
||||
|
||||
this.latestMessages.setAverageWind(averageWind);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
//Unrecognised message.
|
||||
default: {
|
||||
System.out.println("Broken Message!");
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
package visualiser.enums;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The states in which a connection from a client to a server may have.
|
||||
*/
|
||||
public enum ConnectionToServerState {
|
||||
|
||||
UNKNOWN(0),
|
||||
|
||||
/**
|
||||
* We're waiting for the server to complete the joining handshake.
|
||||
* See {@link network.Messages.RequestToJoin} and {@link network.Messages.JoinAcceptance}.
|
||||
*/
|
||||
REQUEST_SENT(1),
|
||||
|
||||
/**
|
||||
* The client has receved a {@link network.Messages.JoinAcceptance} from the server.
|
||||
*/
|
||||
RESPONSE_RECEIVED(2),
|
||||
|
||||
/**
|
||||
* The server has completed the handshake, and is connected.
|
||||
* That is, the client sent a {@link network.Messages.RequestToJoin}, which was successful, and the server responded with a {@link network.Messages.JoinAcceptance}.
|
||||
*/
|
||||
CONNECTED(3),
|
||||
|
||||
/**
|
||||
* The server has timed out, or the connection has been interrupted.
|
||||
*/
|
||||
TIMED_OUT(4),
|
||||
|
||||
/**
|
||||
* The client's connection has been declined.
|
||||
*/
|
||||
DECLINED(5);
|
||||
|
||||
|
||||
|
||||
|
||||
private byte value;
|
||||
|
||||
/**
|
||||
* Ctor. Creates a ConnectionToServerState from a given primitive integer value, cast to a byte.
|
||||
* @param value Integer, which is cast to byte, to construct from.
|
||||
*/
|
||||
private ConnectionToServerState(int value) {
|
||||
this.value = (byte) value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the primitive value of the enum.
|
||||
* @return Primitive value of the enum.
|
||||
*/
|
||||
public byte getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stores a mapping between Byte values and ConnectionToServerState values.
|
||||
*/
|
||||
private static final Map<Byte, ConnectionToServerState> byteToStateMap = new HashMap<>();
|
||||
|
||||
|
||||
/*
|
||||
Static initialization block. Initializes the byteToStateMap.
|
||||
*/
|
||||
static {
|
||||
for (ConnectionToServerState type : ConnectionToServerState.values()) {
|
||||
ConnectionToServerState.byteToStateMap.put(type.value, type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the enumeration value which corresponds to a given byte value.
|
||||
* @param connectionState Byte value to convert to a ConnectionToServerState value.
|
||||
* @return The ConnectionToServerState value which corresponds to the given byte value.
|
||||
*/
|
||||
public static ConnectionToServerState fromByte(byte connectionState) {
|
||||
//Gets the corresponding ConnectionToServerState from the map.
|
||||
ConnectionToServerState type = ConnectionToServerState.byteToStateMap.get(connectionState);
|
||||
|
||||
if (type == null) {
|
||||
//If the byte value wasn't found, return the UNKNOWN ConnectionToServerState.
|
||||
return ConnectionToServerState.UNKNOWN;
|
||||
} else {
|
||||
//Otherwise, return the ConnectionToServerState.
|
||||
return type;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,88 +1,102 @@
|
||||
package visualiser.gameController;
|
||||
|
||||
import mock.model.RaceLogic;
|
||||
import network.BinaryMessageDecoder;
|
||||
import network.Exceptions.InvalidMessageException;
|
||||
import network.MessageDecoders.BoatActionDecoder;
|
||||
import mock.exceptions.CommandConstructionException;
|
||||
import mock.model.MockRace;
|
||||
import mock.model.commandFactory.Command;
|
||||
import mock.model.commandFactory.CommandFactory;
|
||||
import mock.model.commandFactory.CompositeCommand;
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.BoatAction;
|
||||
import network.Messages.Enums.BoatActionEnum;
|
||||
import network.Messages.Enums.MessageType;
|
||||
import shared.model.RunnableWithFramePeriod;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.Observable;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Service for dispatching key press data to race from client
|
||||
*/
|
||||
public class ControllerServer extends Observable implements Runnable {
|
||||
public class ControllerServer implements RunnableWithFramePeriod {
|
||||
|
||||
|
||||
/**
|
||||
* Socket to client
|
||||
* Queue of incoming messages from client.
|
||||
*/
|
||||
private Socket socket;
|
||||
private BlockingQueue<AC35Data> inputQueue;
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for input from client
|
||||
* Collection of commands from client for race to execute.
|
||||
*/
|
||||
private DataInputStream inputStream;
|
||||
private CompositeCommand compositeCommand;
|
||||
|
||||
/**
|
||||
* Last received boat action
|
||||
* The context for each command.
|
||||
*/
|
||||
private BoatActionEnum action;
|
||||
private MockRace raceState;
|
||||
|
||||
/**
|
||||
* Initialise server-side controller with live client socket
|
||||
* @param socket to client
|
||||
* @param race logic loop observing controls
|
||||
* This is the source ID associated with the client.
|
||||
*/
|
||||
public ControllerServer(Socket socket, RaceLogic race) {
|
||||
this.socket = socket;
|
||||
this.addObserver(race);
|
||||
try {
|
||||
this.inputStream = new DataInputStream(this.socket.getInputStream());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
private int clientSourceID;
|
||||
|
||||
|
||||
public BoatActionEnum getAction() {
|
||||
return action;
|
||||
|
||||
/**
|
||||
* Initialise server-side controller with live client socket.
|
||||
* @param compositeCommand Commands for the race to execute.
|
||||
* @param inputQueue The queue of messages to read from.
|
||||
* @param clientSourceID The source ID of the client's boat.
|
||||
* @param raceState The context for each command.
|
||||
*/
|
||||
public ControllerServer(CompositeCommand compositeCommand, BlockingQueue<AC35Data> inputQueue, int clientSourceID, MockRace raceState) {
|
||||
this.compositeCommand = compositeCommand;
|
||||
this.inputQueue = inputQueue;
|
||||
this.clientSourceID = clientSourceID;
|
||||
this.raceState = raceState;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wait for controller key input from client and loop.
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
while(true) {
|
||||
byte[] message = new byte[20];
|
||||
while(!Thread.interrupted()) {
|
||||
|
||||
try {
|
||||
if (inputStream.available() > 0) {
|
||||
|
||||
inputStream.read(message);
|
||||
AC35Data message = inputQueue.take();
|
||||
|
||||
if (message.getType() == MessageType.BOATACTION) {
|
||||
|
||||
BoatAction boatAction = (BoatAction) message;
|
||||
|
||||
BinaryMessageDecoder encodedMessage = new BinaryMessageDecoder(message);
|
||||
BoatActionDecoder boatActionDecoder = new BoatActionDecoder();
|
||||
|
||||
boatAction.setSourceID(clientSourceID);
|
||||
|
||||
try {
|
||||
boatActionDecoder.decode(encodedMessage.getMessageBody());
|
||||
BoatAction boatAction = boatActionDecoder.getMessage();
|
||||
action = boatAction.getBoatAction();
|
||||
Command command = CommandFactory.createCommand(raceState, boatAction);
|
||||
compositeCommand.addCommand(command);
|
||||
|
||||
// Notify observers of most recent action
|
||||
this.notifyObservers();
|
||||
this.setChanged();
|
||||
} catch (CommandConstructionException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "ControllerServer could not create a Command for BoatAction: " + boatAction + ".", e);
|
||||
|
||||
} catch (InvalidMessageException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "Could not decode BoatAction message.", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "ControllerServer Interrupted while waiting for message on incoming message queue.", e);
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,469 +0,0 @@
|
||||
package visualiser.model;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.paint.Color;
|
||||
import network.Messages.BoatLocation;
|
||||
import network.Messages.BoatStatus;
|
||||
import network.Messages.Enums.BoatStatusEnum;
|
||||
import network.Messages.Enums.RaceStatusEnum;
|
||||
import network.Messages.LatestMessages;
|
||||
import network.Messages.RaceStatus;
|
||||
import shared.dataInput.BoatDataSource;
|
||||
import shared.dataInput.RaceDataSource;
|
||||
import shared.dataInput.RegattaDataSource;
|
||||
import shared.model.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* The Class used to view the race streamed.
|
||||
* Has a course, boats, boundaries, etc...
|
||||
* Observes LatestMessages and updates its state based on new messages.
|
||||
*/
|
||||
public class VisualiserRace extends Race implements Runnable {
|
||||
|
||||
|
||||
/**
|
||||
* An observable list of boats in the race.
|
||||
*/
|
||||
private final ObservableList<VisualiserBoat> boats;
|
||||
|
||||
/**
|
||||
* An observable list of marker boats in the race.
|
||||
*/
|
||||
private ObservableList<Mark> boatMarkers;
|
||||
|
||||
|
||||
/**
|
||||
* Maps between a Leg to a list of boats, in the order that they finished the leg.
|
||||
* Used by the Sparkline to ensure it has correct information.
|
||||
*/
|
||||
private Map<Leg, List<VisualiserBoat>> legCompletionOrder = new HashMap<>();
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a race object with a given RaceDataSource, BoatDataSource, and RegattaDataSource and receives events from LatestMessages.
|
||||
* @param boatDataSource Data source for boat related data (yachts and marker boats).
|
||||
* @param raceDataSource Data source for race related data (participating boats, legs, etc...).
|
||||
* @param regattaDataSource Data source for race related data (course name, location, timezone, etc...).
|
||||
* @param latestMessages The LatestMessages to send events to.
|
||||
* @param colors A collection of colors used to assign a color to each boat.
|
||||
*/
|
||||
public VisualiserRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, List<Color> colors) {
|
||||
|
||||
super(boatDataSource, raceDataSource, regattaDataSource, latestMessages);
|
||||
|
||||
|
||||
this.boats = FXCollections.observableArrayList(this.generateVisualiserBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), colors));
|
||||
|
||||
this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values());
|
||||
|
||||
|
||||
//Initialise the leg completion order map.
|
||||
for (Leg leg : this.legs) {
|
||||
this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the race data source for this race to a new RaceDataSource.
|
||||
* Uses the boundary and legs specified by the new RaceDataSource.
|
||||
* @param raceDataSource The new RaceDataSource to use.
|
||||
*/
|
||||
public void setRaceDataSource(RaceDataSource raceDataSource) {
|
||||
this.raceDataSource = raceDataSource;
|
||||
|
||||
this.boundary = raceDataSource.getBoundary();
|
||||
|
||||
this.useLegsList(raceDataSource.getLegs());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the boat data source for this race to a new BoatDataSource.
|
||||
* Uses the marker boats specified by the new BoatDataSource.
|
||||
* @param boatDataSource The new BoatDataSource to use.
|
||||
*/
|
||||
public void setBoatDataSource(BoatDataSource boatDataSource) {
|
||||
this.boatDataSource = boatDataSource;
|
||||
|
||||
this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the regatta data source for this race to a new RegattaDataSource.
|
||||
* @param regattaDataSource The new RegattaDataSource to use.
|
||||
*/
|
||||
public void setRegattaDataSource(RegattaDataSource regattaDataSource) {
|
||||
this.regattaDataSource = regattaDataSource;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of {@link Mark} boats.
|
||||
* @return List of mark boats.
|
||||
*/
|
||||
public ObservableList<Mark> getMarks() {
|
||||
return boatMarkers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a list of VisualiserBoats given a list of Boats, and a list of participating boats.
|
||||
* @param boats The map of Boats describing boats that are potentially in the race. Maps boat sourceID to boat.
|
||||
* @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating.
|
||||
* @param colors The list of colors to be used for the boats.
|
||||
* @return A list of MockBoats that are participating in the race.
|
||||
*/
|
||||
private List<VisualiserBoat> generateVisualiserBoats(Map<Integer, Boat> boats, List<Integer> sourceIDs, List<Color> colors) {
|
||||
|
||||
List<VisualiserBoat> visualiserBoats = new ArrayList<>(sourceIDs.size());
|
||||
|
||||
//For each sourceID participating...
|
||||
int colorIndex = 0;
|
||||
for (int sourceID : sourceIDs) {
|
||||
|
||||
//Get the boat associated with the sourceID.
|
||||
Boat boat = boats.get(sourceID);
|
||||
|
||||
//Get a color for the boat.
|
||||
Color color = colors.get(colorIndex);
|
||||
|
||||
//Construct a VisualiserBoat using the Boat and Polars.
|
||||
VisualiserBoat visualiserBoat = new VisualiserBoat(boat, color);
|
||||
|
||||
visualiserBoats.add(visualiserBoat);
|
||||
|
||||
//Next color.
|
||||
colorIndex++;
|
||||
|
||||
}
|
||||
|
||||
return visualiserBoats;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialise the boats in the race.
|
||||
* This sets their current leg.
|
||||
*/
|
||||
@Override
|
||||
protected void initialiseBoats() {
|
||||
|
||||
Leg startingLeg = legs.get(0);
|
||||
|
||||
for (VisualiserBoat boat : boats) {
|
||||
|
||||
boat.setCurrentLeg(startingLeg);
|
||||
boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates all of the racing boats based on messages received.
|
||||
* @param boats The list of racing boats.
|
||||
* @param boatLocationMap A map between boat sourceIDs and BoatLocation messages.
|
||||
* @param boatStatusMap A map between boat sourceIDs and BoatStatus messages.
|
||||
*/
|
||||
private void updateBoats(ObservableList<VisualiserBoat> boats, Map<Integer, BoatLocation> boatLocationMap, Map<Integer, BoatStatus> boatStatusMap) {
|
||||
|
||||
for (VisualiserBoat boat : boats) {
|
||||
BoatLocation boatLocation = boatLocationMap.get(boat.getSourceID());
|
||||
BoatStatus boatStatus = boatStatusMap.get(boat.getSourceID());
|
||||
updateBoat(boat, boatLocation, boatStatus);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates an individual racing boat based on messages received.
|
||||
* @param boat The boat to update.
|
||||
* @param boatLocation The BoatLocation message to use.
|
||||
* @param boatStatus The BoatStatus message to use.
|
||||
*/
|
||||
private void updateBoat(VisualiserBoat boat, BoatLocation boatLocation, BoatStatus boatStatus) {
|
||||
|
||||
if (boatLocation != null && boatStatus != null) {
|
||||
|
||||
//Get the new position.
|
||||
double latitude = boatLocation.getLatitude();
|
||||
double longitude = boatLocation.getLongitude();
|
||||
GPSCoordinate gpsCoordinate = new GPSCoordinate(latitude, longitude);
|
||||
|
||||
boat.setCurrentPosition(gpsCoordinate);
|
||||
|
||||
//Bearing.
|
||||
boat.setBearing(boatLocation.getHeading());
|
||||
|
||||
//Time until next mark.
|
||||
boat.setEstimatedTimeAtNextMark(raceClock.getLocalTime(boatStatus.getEstTimeAtNextMark()));
|
||||
|
||||
//Speed.
|
||||
boat.setCurrentSpeed(boatLocation.getBoatSpeedKnots());
|
||||
|
||||
|
||||
//Boat status.
|
||||
BoatStatusEnum newBoatStatusEnum = boatStatus.getBoatStatus();
|
||||
|
||||
//If we are changing from non-racing to racing, we need to initialise boat with their time at last mark.
|
||||
if ((boat.getStatus() != BoatStatusEnum.RACING) && (newBoatStatusEnum == BoatStatusEnum.RACING)) {
|
||||
boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
|
||||
}
|
||||
|
||||
boat.setStatus(newBoatStatusEnum);
|
||||
|
||||
|
||||
//Leg.
|
||||
int legNumber = boatStatus.getLegNumber();
|
||||
|
||||
if (legNumber >= 1 && legNumber < legs.size()) {
|
||||
if (boat.getCurrentLeg() != legs.get(legNumber)) {
|
||||
boatFinishedLeg(boat, legs.get(legNumber));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Attempt to add a track point.
|
||||
if (newBoatStatusEnum == BoatStatusEnum.RACING) {
|
||||
boat.addTrackPoint(boat.getCurrentPosition(), raceClock.getCurrentTime());
|
||||
}
|
||||
|
||||
//Set finish time if boat finished.
|
||||
if (newBoatStatusEnum == BoatStatusEnum.FINISHED || legNumber == this.legs.size()) {
|
||||
boat.setTimeFinished(boatLocation.getTime());
|
||||
boat.setStatus(BoatStatusEnum.FINISHED);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates a boat's leg to a specified leg. Also records the order in which the boat passed the leg.
|
||||
* @param boat The boat to update.
|
||||
* @param leg The leg to use.
|
||||
*/
|
||||
private void boatFinishedLeg(VisualiserBoat boat, Leg leg) {
|
||||
|
||||
//Record order in which boat finished leg.
|
||||
this.legCompletionOrder.get(boat.getCurrentLeg()).add(boat);
|
||||
|
||||
//Update boat.
|
||||
boat.setCurrentLeg(leg);
|
||||
boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates all of the marker boats based on messages received.
|
||||
* @param boatMarkers The list of marker boats.
|
||||
* @param boatLocationMap A map between boat sourceIDs and BoatLocation messages.
|
||||
* @param boatStatusMap A map between boat sourceIDs and BoatStatus messages.
|
||||
*/
|
||||
private void updateMarkers(ObservableList<Mark> boatMarkers, Map<Integer, BoatLocation> boatLocationMap, Map<Integer, BoatStatus> boatStatusMap) {
|
||||
|
||||
for (Mark mark : boatMarkers) {
|
||||
BoatLocation boatLocation = boatLocationMap.get(mark.getSourceID());
|
||||
updateMark(mark, boatLocation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an individual marker boat based on messages received.
|
||||
* @param mark The marker boat to be updated.
|
||||
* @param boatLocation The message describing the boat's new location.
|
||||
*/
|
||||
private void updateMark(Mark mark, BoatLocation boatLocation) {
|
||||
|
||||
if (boatLocation != null) {
|
||||
|
||||
//We only update the boat's position.
|
||||
double latitude = boatLocation.getLatitude();
|
||||
double longitude = boatLocation.getLongitude();
|
||||
GPSCoordinate gpsCoordinate = new GPSCoordinate(latitude, longitude);
|
||||
|
||||
mark.setPosition(gpsCoordinate);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the race status (RaceStatusEnum, wind bearing, wind speed) based on received messages.
|
||||
* @param raceStatus The RaceStatus message received.
|
||||
*/
|
||||
private void updateRaceStatus(RaceStatus raceStatus) {
|
||||
|
||||
//Race status enum.
|
||||
this.raceStatusEnum = raceStatus.getRaceStatus();
|
||||
|
||||
//Wind.
|
||||
this.setWind(
|
||||
raceStatus.getWindDirection(),
|
||||
raceStatus.getWindSpeed() );
|
||||
|
||||
//Current race time.
|
||||
this.raceClock.setUTCTime(raceStatus.getCurrentTime());
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Runnable for the thread.
|
||||
*/
|
||||
public void run() {
|
||||
initialiseBoats();
|
||||
startRaceStream();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Starts the race.
|
||||
* This updates the race based on {@link #latestMessages}.
|
||||
*/
|
||||
private void startRaceStream() {
|
||||
|
||||
new AnimationTimer() {
|
||||
|
||||
long lastFrameTime = System.currentTimeMillis();
|
||||
|
||||
@Override
|
||||
public void handle(long arg0) {
|
||||
|
||||
//Calculate the frame period.
|
||||
long currentFrameTime = System.currentTimeMillis();
|
||||
long framePeriod = currentFrameTime - lastFrameTime;
|
||||
|
||||
|
||||
//Update race status.
|
||||
updateRaceStatus(latestMessages.getRaceStatus());
|
||||
|
||||
|
||||
//Update racing boats.
|
||||
updateBoats(boats, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap());
|
||||
//And their positions (e.g., 5th).
|
||||
updateBoatPositions(boats);
|
||||
|
||||
|
||||
//Update marker boats.
|
||||
updateMarkers(boatMarkers, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap());
|
||||
|
||||
|
||||
|
||||
|
||||
if (getRaceStatusEnum() == RaceStatusEnum.FINISHED) {
|
||||
stop();
|
||||
}
|
||||
|
||||
lastFrameTime = currentFrameTime;
|
||||
|
||||
//Increment fps.
|
||||
incrementFps(framePeriod);
|
||||
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update position of boats in race (e.g, 5th), no position if on starting leg or DNF.
|
||||
* @param boats The list of boats to update.
|
||||
*/
|
||||
private void updateBoatPositions(ObservableList<VisualiserBoat> boats) {
|
||||
|
||||
//Sort boats.
|
||||
sortBoatsByPosition(boats);
|
||||
|
||||
//Assign new positions.
|
||||
for (int i = 0; i < boats.size(); i++) {
|
||||
VisualiserBoat boat = boats.get(i);
|
||||
|
||||
|
||||
if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) {
|
||||
|
||||
boat.setPosition("-");
|
||||
|
||||
} else {
|
||||
boat.setPosition(Integer.toString(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the list of boats by their position within the race.
|
||||
* @param boats The list of boats in the race.
|
||||
*/
|
||||
private void sortBoatsByPosition(ObservableList<VisualiserBoat> boats) {
|
||||
|
||||
FXCollections.sort(boats, (a, b) -> {
|
||||
//Get the difference in leg numbers.
|
||||
int legNumberDelta = b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber();
|
||||
|
||||
//If they're on the same leg, we need to compare time to finish leg.
|
||||
if (legNumberDelta == 0) {
|
||||
return (int) Duration.between(b.getEstimatedTimeAtNextMark(), a.getEstimatedTimeAtNextMark()).toMillis();
|
||||
} else {
|
||||
return legNumberDelta;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the boats participating in the race.
|
||||
* @return ObservableList of boats participating in the race.
|
||||
*/
|
||||
public ObservableList<VisualiserBoat> getBoats() {
|
||||
return boats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the order in which boats completed each leg. Maps the leg to a list of boats, ordered by the order in which they finished the leg.
|
||||
* @return Leg completion order for each leg.
|
||||
*/
|
||||
public Map<Leg, List<VisualiserBoat>> getLegCompletionOrder() {
|
||||
return legCompletionOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an estimated time an event will occur, and converts it to the
|
||||
* number of seconds before the event will occur.
|
||||
*
|
||||
* @param estTimeMillis The estimated time, in milliseconds.
|
||||
* @param currentTime The current time, in milliseconds.
|
||||
* @return int difference between time the race started and the estimated time
|
||||
*/
|
||||
private int convertEstTime(long estTimeMillis, long currentTime) {
|
||||
|
||||
//Calculate millisecond delta.
|
||||
long estElapsedMillis = estTimeMillis - currentTime;
|
||||
|
||||
//Convert milliseconds to seconds.
|
||||
int estElapsedSecs = Math.round(estElapsedMillis / 1000);
|
||||
|
||||
return estElapsedSecs;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package visualiser.model;
|
||||
|
||||
import mock.exceptions.CommandConstructionException;
|
||||
import mock.model.commandFactory.Command;
|
||||
import mock.model.commandFactory.CompositeCommand;
|
||||
import network.Messages.*;
|
||||
import shared.model.RunnableWithFramePeriod;
|
||||
import visualiser.Commands.VisualiserRaceCommands.VisualiserRaceCommandFactory;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* The controller for race related messages, coming from the server to the client.
|
||||
*/
|
||||
public class VisualiserRaceController implements RunnableWithFramePeriod {
|
||||
|
||||
|
||||
/**
|
||||
* Incoming messages from server.
|
||||
*/
|
||||
private BlockingQueue<AC35Data> incomingMessages;
|
||||
|
||||
|
||||
/**
|
||||
* Commands are placed in here, and executed by visualiserRace.
|
||||
*/
|
||||
private CompositeCommand compositeRaceCommand;
|
||||
|
||||
|
||||
/**
|
||||
* The context that created commands operate on.
|
||||
*/
|
||||
private VisualiserRaceState visualiserRace;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a visualiserInput to convert an incoming stream of messages into commands.
|
||||
* @param incomingMessages The incoming queue of messages.
|
||||
* @param visualiserRace The context to for commands to operate on.
|
||||
* @param compositeRaceCommand The composite command to place command in.
|
||||
*/
|
||||
public VisualiserRaceController(BlockingQueue<AC35Data> incomingMessages, VisualiserRaceState visualiserRace, CompositeCommand compositeRaceCommand) {
|
||||
this.incomingMessages = incomingMessages;
|
||||
this.compositeRaceCommand = compositeRaceCommand;
|
||||
this.visualiserRace = visualiserRace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
while (!Thread.interrupted()) {
|
||||
|
||||
try {
|
||||
AC35Data message = incomingMessages.take();
|
||||
|
||||
Command command = VisualiserRaceCommandFactory.create(message, visualiserRace);
|
||||
compositeRaceCommand.addCommand(command);
|
||||
|
||||
} catch (CommandConstructionException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "VisualiserRaceController could not create a command for incoming message.");
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Logger.getGlobal().log(Level.SEVERE, "VisualiserRaceController was interrupted on thread: " + Thread.currentThread() + " while waiting for messages.");
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
package visualiser.model;
|
||||
|
||||
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import mock.model.commandFactory.CompositeCommand;
|
||||
import network.Messages.Enums.RequestToJoinEnum;
|
||||
import shared.dataInput.EmptyBoatDataSource;
|
||||
import shared.dataInput.EmptyRaceDataSource;
|
||||
import shared.dataInput.EmptyRegattaDataSource;
|
||||
import visualiser.gameController.ControllerClient;
|
||||
import visualiser.network.ServerConnection;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
|
||||
|
||||
/**
|
||||
* This class holds a race, and a client's connection to it
|
||||
*/
|
||||
public class VisualiserRaceEvent {
|
||||
|
||||
/**
|
||||
* Our connection to the server.
|
||||
*/
|
||||
private ServerConnection serverConnection;
|
||||
/**
|
||||
* The thread serverConnection is running on.
|
||||
*/
|
||||
private Thread serverConnectionThread;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The race object which describes the currently occurring race.
|
||||
*/
|
||||
private VisualiserRaceState visualiserRaceState;
|
||||
|
||||
|
||||
/**
|
||||
* The service for updating the {@link #visualiserRaceState}.
|
||||
*/
|
||||
private VisualiserRaceService visualiserRaceService;
|
||||
/**
|
||||
* The thread {@link #visualiserRaceService} is running on.
|
||||
*/
|
||||
private Thread visualiserRaceServiceThread;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a visualiser race event, with a given socket and request type.
|
||||
* @param socket The socket to connect to.
|
||||
* @param requestType The type of {@link network.Messages.RequestToJoin} to make.
|
||||
* @throws IOException Thrown if there is a problem with the socket.
|
||||
*/
|
||||
public VisualiserRaceEvent(Socket socket, RequestToJoinEnum requestType) throws IOException {
|
||||
|
||||
this.visualiserRaceState = new VisualiserRaceState(new EmptyRaceDataSource(), new EmptyRegattaDataSource(), new EmptyBoatDataSource());
|
||||
|
||||
|
||||
CompositeCommand raceCommands = new CompositeCommand();
|
||||
this.visualiserRaceService = new VisualiserRaceService(raceCommands, visualiserRaceState);
|
||||
|
||||
this.visualiserRaceServiceThread = new Thread(visualiserRaceService, "VisualiserRaceEvent()->VisualiserRaceService thread " + visualiserRaceService);
|
||||
this.visualiserRaceServiceThread.start();
|
||||
|
||||
|
||||
this.serverConnection = new ServerConnection(socket, visualiserRaceState, raceCommands, requestType);
|
||||
this.serverConnectionThread = new Thread(serverConnection, "StartController.enterLobby()->serverConnection thread " + serverConnection);
|
||||
this.serverConnectionThread.start();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the state of the race.
|
||||
* @return The state of the race.
|
||||
*/
|
||||
public VisualiserRaceState getVisualiserRaceState() {
|
||||
return visualiserRaceState;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the controller client, which writes BoatAction messages to the outgoing queue.
|
||||
* @return The ControllerClient.
|
||||
*/
|
||||
public ControllerClient getControllerClient() {
|
||||
return serverConnection.getControllerClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the connection to server.
|
||||
* @return Connection to server.
|
||||
*/
|
||||
public ServerConnection getServerConnection() {
|
||||
return serverConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the framerate property of the race.
|
||||
* @return Framerate property of race.
|
||||
*/
|
||||
public IntegerProperty getFrameRateProperty() {
|
||||
return visualiserRaceService.getFrameRateProperty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Terminates the server connection and race service.
|
||||
*/
|
||||
public void terminate() {
|
||||
this.serverConnectionThread.interrupt();
|
||||
|
||||
this.visualiserRaceServiceThread.interrupt();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package visualiser.model;
|
||||
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import mock.model.commandFactory.CompositeCommand;
|
||||
import shared.model.FrameRateTracker;
|
||||
import shared.model.RunnableWithFramePeriod;
|
||||
|
||||
|
||||
/**
|
||||
* Handles updating a {@link VisualiserRaceState} with incoming commands.
|
||||
*/
|
||||
public class VisualiserRaceService implements RunnableWithFramePeriod {
|
||||
|
||||
|
||||
/**
|
||||
* The race state to update.
|
||||
*/
|
||||
private VisualiserRaceState visualiserRaceState;
|
||||
|
||||
|
||||
/**
|
||||
* A composite commands to execute to update the race.
|
||||
*/
|
||||
private CompositeCommand raceCommands;
|
||||
|
||||
|
||||
/**
|
||||
* Used to track the framerate of the "simulation".
|
||||
*/
|
||||
private FrameRateTracker frameRateTracker;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a visualiser race which models a yacht race, and is modified by CompositeCommand.
|
||||
* @param raceCommands A composite commands to execute to update the race.
|
||||
* @param visualiserRaceState The race state to update.
|
||||
*/
|
||||
public VisualiserRaceService(CompositeCommand raceCommands, VisualiserRaceState visualiserRaceState) {
|
||||
this.raceCommands = raceCommands;
|
||||
this.visualiserRaceState = visualiserRaceState;
|
||||
|
||||
this.frameRateTracker = new FrameRateTracker();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the CompositeCommand executed by the race.
|
||||
* @return CompositeCommand executed by race.
|
||||
*/
|
||||
public CompositeCommand getRaceCommands() {
|
||||
return raceCommands;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
long previousFrameTime = System.currentTimeMillis();
|
||||
|
||||
while (!Thread.interrupted()) {
|
||||
|
||||
long currentFrameTime = System.currentTimeMillis();
|
||||
|
||||
waitForFramePeriod(previousFrameTime, currentFrameTime, 16);
|
||||
|
||||
previousFrameTime = currentFrameTime;
|
||||
|
||||
|
||||
raceCommands.execute();
|
||||
|
||||
}
|
||||
|
||||
frameRateTracker.stop();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the framerate property of the race.
|
||||
* @return Framerate property of race.
|
||||
*/
|
||||
public IntegerProperty getFrameRateProperty() {
|
||||
return frameRateTracker.fpsProperty();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,408 @@
|
||||
package visualiser.model;
|
||||
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.paint.Color;
|
||||
import network.Messages.Enums.BoatStatusEnum;
|
||||
import shared.dataInput.BoatDataSource;
|
||||
import shared.dataInput.RaceDataSource;
|
||||
import shared.dataInput.RegattaDataSource;
|
||||
import shared.exceptions.BoatNotFoundException;
|
||||
import shared.exceptions.MarkNotFoundException;
|
||||
import shared.model.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
/**
|
||||
* This class contains all of the state of a race on the client (visualiser) side.
|
||||
*/
|
||||
public class VisualiserRaceState extends RaceState {
|
||||
|
||||
|
||||
/**
|
||||
* A list of boats in the race.
|
||||
*/
|
||||
private ObservableList<VisualiserBoat> boats;
|
||||
|
||||
/**
|
||||
* The source ID of the boat assigned to the player.
|
||||
* 0 if no boat has been assigned.
|
||||
*/
|
||||
private int playerBoatID;
|
||||
|
||||
|
||||
/**
|
||||
* Maps between a Leg to a list of boats, in the order that they finished the leg.
|
||||
* Used by the Sparkline to ensure it has correct information.
|
||||
* TODO BUG: if we receive a race.xml file during the race, then we need to add/remove legs to this, without losing information.
|
||||
*/
|
||||
private Map<Leg, List<VisualiserBoat>> legCompletionOrder;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor.
|
||||
*/
|
||||
private List<Color> unassignedColors = new ArrayList<>(Arrays.asList(
|
||||
Color.BLUEVIOLET,
|
||||
Color.BLACK,
|
||||
Color.RED,
|
||||
Color.ORANGE,
|
||||
Color.DARKOLIVEGREEN,
|
||||
Color.LIMEGREEN,
|
||||
Color.PURPLE,
|
||||
Color.DARKGRAY,
|
||||
Color.YELLOW
|
||||
//TODO may need to add more colors.
|
||||
));
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a visualiser race which models a yacht race.
|
||||
* @param raceDataSource The raceDataSource to initialise with.
|
||||
* @param regattaDataSource The regattaDataSource to initialise with.
|
||||
* @param boatDataSource The boatDataSource to initialise with.
|
||||
*/
|
||||
public VisualiserRaceState(RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, BoatDataSource boatDataSource) {
|
||||
|
||||
this.boats = FXCollections.observableArrayList();
|
||||
|
||||
this.playerBoatID = 0;
|
||||
|
||||
this.legCompletionOrder = new HashMap<>();
|
||||
|
||||
|
||||
setRaceDataSource(raceDataSource);
|
||||
setRegattaDataSource(regattaDataSource);
|
||||
setBoatDataSource(boatDataSource);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Sets the race data source for this race to a new RaceDataSource.
|
||||
* Uses the boundary and legs specified by the new RaceDataSource.
|
||||
* @param raceDataSource The new RaceDataSource to use.
|
||||
*/
|
||||
public void setRaceDataSource(RaceDataSource raceDataSource) {
|
||||
super.setRaceDataSource(raceDataSource);
|
||||
|
||||
if (getBoatDataSource() != null) {
|
||||
this.generateVisualiserBoats(this.boats, getBoatDataSource().getBoats(), raceDataSource.getParticipants(), unassignedColors);
|
||||
}
|
||||
|
||||
initialiseLegCompletionOrder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the boat data source for this race to a new BoatDataSource.
|
||||
* Uses the marker boats specified by the new BoatDataSource.
|
||||
* @param boatDataSource The new BoatDataSource to use.
|
||||
*/
|
||||
public void setBoatDataSource(BoatDataSource boatDataSource) {
|
||||
super.setBoatDataSource(boatDataSource);
|
||||
|
||||
if (getRaceDataSource() != null) {
|
||||
this.generateVisualiserBoats(this.boats, boatDataSource.getBoats(), getRaceDataSource().getParticipants(), unassignedColors);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the regatta data source for this race to a new RegattaDataSource.
|
||||
* @param regattaDataSource The new RegattaDataSource to use.
|
||||
*/
|
||||
public void setRegattaDataSource(RegattaDataSource regattaDataSource) {
|
||||
super.setRegattaDataSource(regattaDataSource);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialises the {@link #legCompletionOrder} map.
|
||||
*/
|
||||
public void initialiseLegCompletionOrder() {
|
||||
//Initialise the leg completion order map.
|
||||
for (Leg leg : getLegs()) {
|
||||
this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Generates a list of VisualiserBoats given a list of Boats, and a list of participating boats.
|
||||
* This will add VisualiserBoats for newly participating sourceID, and remove VisualiserBoats for any participating sourceIDs that have been removed.
|
||||
*
|
||||
* @param existingBoats The visualiser boats that already exist in the race. This will be populated when we receive a new race.xml or boats.xml.
|
||||
* @param boats The map of {@link Boat}s describing boats that are potentially in the race. Maps boat sourceID to boat.
|
||||
* @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating.
|
||||
* @param colors The list of unassignedColors to be used for the boats.
|
||||
*/
|
||||
private void generateVisualiserBoats(ObservableList<VisualiserBoat> existingBoats, Map<Integer, Boat> boats, List<Integer> sourceIDs, List<Color> colors) {
|
||||
|
||||
//Remove any VisualiserBoats that are no longer participating.
|
||||
for (VisualiserBoat boat : new ArrayList<>(existingBoats)) {
|
||||
|
||||
//Boat no longer is participating.
|
||||
if (!sourceIDs.contains(boat.getSourceID())) {
|
||||
//Return their colors to the color list.
|
||||
colors.add(boat.getColor());
|
||||
|
||||
//Remove boat.
|
||||
existingBoats.remove(boat);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Get source IDs of already existing boats.
|
||||
List<Integer> existingBoatIDs = new ArrayList<>();
|
||||
for (VisualiserBoat boat : existingBoats) {
|
||||
existingBoatIDs.add(boat.getSourceID());
|
||||
}
|
||||
|
||||
//Get source IDs of only newly participating boats.
|
||||
List<Integer> newBoatIDs = new ArrayList<>(sourceIDs);
|
||||
newBoatIDs.removeAll(existingBoatIDs);
|
||||
|
||||
//Create VisualiserBoat for newly participating boats.
|
||||
for (Integer sourceID : newBoatIDs) {
|
||||
|
||||
if (boats.containsKey(sourceID)) {
|
||||
|
||||
VisualiserBoat boat = new VisualiserBoat(
|
||||
boats.get(sourceID),
|
||||
colors.remove(colors.size() - 1));//TODO potential bug: not enough colors for boats.
|
||||
|
||||
boat.setCurrentLeg(getLegs().get(0));
|
||||
|
||||
existingBoats.add(boat);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setPlayerBoat();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the boat the player has been assigned to as belonging to them.
|
||||
*/
|
||||
private void setPlayerBoat() {
|
||||
|
||||
if (getPlayerBoatID() != 0) {
|
||||
|
||||
for (VisualiserBoat boat : new ArrayList<>(getBoats())) {
|
||||
|
||||
if (boat.getSourceID() == getPlayerBoatID()) {
|
||||
boat.setClientBoat(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialise the boats in the race.
|
||||
* This sets their current leg.
|
||||
*/
|
||||
@Override
|
||||
protected void initialiseBoats() {
|
||||
|
||||
Leg startingLeg = getLegs().get(0);
|
||||
|
||||
for (VisualiserBoat boat : boats) {
|
||||
|
||||
boat.setCurrentLeg(startingLeg);
|
||||
boat.setTimeAtLastMark(getRaceClock().getCurrentTime());
|
||||
boat.setCurrentPosition(new GPSCoordinate(0, 0));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Update position of boats in race (e.g, 5th), no position if on starting leg or DNF.
|
||||
* @param boats The list of boats to update.
|
||||
*/
|
||||
public void updateBoatPositions(List<VisualiserBoat> boats) {
|
||||
|
||||
//Sort boats.
|
||||
sortBoatsByPosition(boats);
|
||||
|
||||
//Assign new positions.
|
||||
for (int i = 0; i < boats.size(); i++) {
|
||||
VisualiserBoat boat = boats.get(i);
|
||||
|
||||
|
||||
if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) {
|
||||
|
||||
boat.setPosition("-");
|
||||
|
||||
} else {
|
||||
boat.setPosition(Integer.toString(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sorts the list of boats by their position within the race.
|
||||
* @param boats The list of boats in the race.
|
||||
*/
|
||||
private void sortBoatsByPosition(List<VisualiserBoat> boats) {
|
||||
|
||||
boats.sort((a, b) -> {
|
||||
//Get the difference in leg numbers.
|
||||
int legNumberDelta = b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber();
|
||||
|
||||
//If they're on the same leg, we need to compare time to finish leg.
|
||||
if (legNumberDelta == 0) {
|
||||
|
||||
//These are potentially null until we receive our first RaceStatus containing BoatStatuses.
|
||||
if ((a.getEstimatedTimeAtNextMark() != null) && (b.getEstimatedTimeAtNextMark() != null)) {
|
||||
|
||||
return (int) Duration.between(
|
||||
b.getEstimatedTimeAtNextMark(),
|
||||
a.getEstimatedTimeAtNextMark() ).toMillis();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return legNumberDelta;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the boats participating in the race.
|
||||
* @return List of boats participating in the race.
|
||||
*/
|
||||
public ObservableList<VisualiserBoat> getBoats() {
|
||||
return boats;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a boat by sourceID.
|
||||
* @param sourceID The source ID the boat.
|
||||
* @return The boat.
|
||||
* @throws BoatNotFoundException Thrown if there is no boat with the specified sourceID.
|
||||
*/
|
||||
public VisualiserBoat getBoat(int sourceID) throws BoatNotFoundException {
|
||||
|
||||
for (VisualiserBoat boat : boats) {
|
||||
|
||||
if (boat.getSourceID() == sourceID) {
|
||||
return boat;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new BoatNotFoundException("Boat with sourceID: " + sourceID + " was not found.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not there exists a {@link VisualiserBoat} with the given source ID.
|
||||
* @param sourceID SourceID of VisualiserBoat.
|
||||
* @return True if VisualiserBoat exists, false otherwise.
|
||||
*/
|
||||
public boolean isVisualiserBoat(int sourceID) {
|
||||
|
||||
try {
|
||||
getBoat(sourceID);
|
||||
return true;
|
||||
} catch (BoatNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a mark by sourceID.
|
||||
* @param sourceID The source ID the mark.
|
||||
* @return The mark.
|
||||
* @throws MarkNotFoundException Thrown if there is no mark with the specified sourceID.
|
||||
*/
|
||||
public Mark getMark(int sourceID) throws MarkNotFoundException {
|
||||
|
||||
for (Mark mark : getMarks()) {
|
||||
|
||||
if (mark.getSourceID() == sourceID) {
|
||||
return mark;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
throw new MarkNotFoundException("Mark with sourceID: " + sourceID + " was not found.");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether or not there exists a {@link Mark} with the given source ID.
|
||||
* @param sourceID SourceID of mark.
|
||||
* @return True if mark exists, false otherwise.
|
||||
*/
|
||||
public boolean isMark(int sourceID) {
|
||||
|
||||
try {
|
||||
getMark(sourceID);
|
||||
return true;
|
||||
} catch (MarkNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns the order in which boats completed each leg. Maps the leg to a list of boats, ordered by the order in which they finished the leg.
|
||||
* @return Leg completion order for each leg.
|
||||
*/
|
||||
public Map<Leg, List<VisualiserBoat>> getLegCompletionOrder() {
|
||||
return legCompletionOrder;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the source ID of the player's boat. 0 if not assigned.
|
||||
* @return Players boat source ID.
|
||||
*/
|
||||
public int getPlayerBoatID() {
|
||||
return playerBoatID;
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the source ID of the player's boat. 0 if not assigned.
|
||||
* @param playerBoatID Players boat source ID.
|
||||
*/
|
||||
public void setPlayerBoatID(int playerBoatID) {
|
||||
this.playerBoatID = playerBoatID;
|
||||
setPlayerBoat();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
package visualiser.network;
|
||||
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.Enums.RequestToJoinEnum;
|
||||
import network.Messages.JoinAcceptance;
|
||||
import network.Messages.RequestToJoin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import shared.model.RunnableWithFramePeriod;
|
||||
import visualiser.enums.ConnectionToServerState;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* This class tracks the state of the connection to a server.
|
||||
*/
|
||||
public class ConnectionToServer implements RunnableWithFramePeriod {
|
||||
|
||||
|
||||
/**
|
||||
* The state of the connection to the client.
|
||||
*/
|
||||
private ConnectionToServerState connectionState = ConnectionToServerState.UNKNOWN;
|
||||
|
||||
|
||||
/**
|
||||
* The type of join request to make to server.
|
||||
*/
|
||||
private RequestToJoinEnum requestType;
|
||||
|
||||
|
||||
/**
|
||||
* The queue to place outgoing messages on.
|
||||
*/
|
||||
private BlockingQueue<AC35Data> outgoingMessages;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The {@link JoinAcceptance} message that has been received, if any.
|
||||
*/
|
||||
@Nullable
|
||||
private JoinAcceptance joinAcceptance;
|
||||
|
||||
|
||||
/**
|
||||
* The incoming commands to execute.
|
||||
*/
|
||||
private BlockingQueue<Command> incomingCommands;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a ConnectionToServer with a given state.
|
||||
* @param connectionState The state of the connection.
|
||||
* @param requestType The type of join request to make to server.
|
||||
* @param incomingCommands The queue of commands to execute.
|
||||
* @param outgoingMessages The queue to place outgoing messages on.
|
||||
*/
|
||||
public ConnectionToServer(ConnectionToServerState connectionState, RequestToJoinEnum requestType, BlockingQueue<Command> incomingCommands, BlockingQueue<AC35Data> outgoingMessages) {
|
||||
this.connectionState = connectionState;
|
||||
this.requestType = requestType;
|
||||
this.incomingCommands = incomingCommands;
|
||||
this.outgoingMessages = outgoingMessages;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the state of this connection.
|
||||
* @return The state of this connection.
|
||||
*/
|
||||
public ConnectionToServerState getConnectionState() {
|
||||
return connectionState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state of this connection.
|
||||
* @param connectionState The new state of this connection.
|
||||
*/
|
||||
public void setConnectionState(ConnectionToServerState connectionState) {
|
||||
this.connectionState = connectionState;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the {@link JoinAcceptance} message received from the server, if any.
|
||||
* @return The JoinAcceptance message from server. Null if no response from server.
|
||||
*/
|
||||
@Nullable
|
||||
public JoinAcceptance getJoinAcceptance() {
|
||||
return joinAcceptance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link JoinAcceptance} message received from the server, if any.
|
||||
* @param joinAcceptance The new JoinAcceptance message from server.
|
||||
*/
|
||||
public void setJoinAcceptance(@NotNull JoinAcceptance joinAcceptance) {
|
||||
this.joinAcceptance = joinAcceptance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
sendRequestToJoinMessage(requestType);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Logger.getGlobal().log(Level.SEVERE, "ConnectionToServer: " + this + " was interrupted on thread: " + Thread.currentThread() + " while sending RequestToJoin.", e);
|
||||
Thread.currentThread().interrupt();
|
||||
|
||||
}
|
||||
|
||||
while (!Thread.interrupted()) {
|
||||
|
||||
try {
|
||||
Command command = incomingCommands.take();
|
||||
command.execute();
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Logger.getGlobal().log(Level.SEVERE, "ConnectionToServer: " + this + " was interrupted on thread: " + Thread.currentThread() + " while reading command.", e);
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//If we get interrupted, we consider the connection to have timed-out.
|
||||
connectionState = ConnectionToServerState.TIMED_OUT;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Sends the server a {@link RequestToJoin} message.
|
||||
* @param requestType The type of request to send
|
||||
* @throws InterruptedException Thrown if the thread is interrupted while placing message on the outgoing message queue.
|
||||
*/
|
||||
private void sendRequestToJoinMessage(RequestToJoinEnum requestType) throws InterruptedException {
|
||||
|
||||
//Send them the source ID.
|
||||
RequestToJoin requestToJoin = new RequestToJoin(requestType);
|
||||
|
||||
send(requestToJoin);
|
||||
|
||||
connectionState = ConnectionToServerState.REQUEST_SENT;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends a given message to the server, via the {@link #outgoingMessages} queue.
|
||||
* @param message Message to send.
|
||||
* @throws InterruptedException Thrown if thread is interrupted while sending message.
|
||||
*/
|
||||
public void send(AC35Data message) throws InterruptedException {
|
||||
outgoingMessages.put(message);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the type of join request that was made.
|
||||
* @return Type of join request made.
|
||||
*/
|
||||
public RequestToJoinEnum getRequestType() {
|
||||
return requestType;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package visualiser.network;
|
||||
|
||||
|
||||
import mock.exceptions.CommandConstructionException;
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.AC35Data;
|
||||
import shared.model.RunnableWithFramePeriod;
|
||||
import visualiser.Commands.ConnectionToServerCommands.ConnectionToServerCommandFactory;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* The controller for connection related messages, coming from the server to the client.
|
||||
*/
|
||||
public class ConnectionToServerController implements RunnableWithFramePeriod {
|
||||
|
||||
|
||||
/**
|
||||
* The incoming queue of messages to act on.
|
||||
*/
|
||||
private BlockingQueue<AC35Data> incomingMessages;
|
||||
|
||||
|
||||
/**
|
||||
* The connection we are acting on.
|
||||
*/
|
||||
private ConnectionToServer connectionToServer;
|
||||
|
||||
|
||||
/**
|
||||
* The queue to place commands on.
|
||||
*/
|
||||
private BlockingQueue<Command> outgoingCommands;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a {@link ConnectionToServer} controller with the given parameters.
|
||||
* This accepts connection related messages, converts them to commands, and passes them to an outgoing command queue.
|
||||
* @param incomingMessages The message queue to read from.
|
||||
* @param connectionToServer The ConnectionToServer (context) to act on.
|
||||
* @param outgoingCommands The queue to place outgoing commands on.
|
||||
*/
|
||||
public ConnectionToServerController(BlockingQueue<AC35Data> incomingMessages, ConnectionToServer connectionToServer, BlockingQueue<Command> outgoingCommands) {
|
||||
this.incomingMessages = incomingMessages;
|
||||
this.connectionToServer = connectionToServer;
|
||||
this.outgoingCommands = outgoingCommands;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
while (!Thread.interrupted()) {
|
||||
|
||||
try {
|
||||
AC35Data message = incomingMessages.take();
|
||||
Command command = ConnectionToServerCommandFactory.create(message, connectionToServer);
|
||||
outgoingCommands.put(command);
|
||||
|
||||
} catch (CommandConstructionException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "ConnectionToServerController: " + this + " could not create command from message.", e);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Logger.getGlobal().log(Level.SEVERE, "ConnectionToServerController: " + this + " was interrupted on thread: " + Thread.currentThread(), e);
|
||||
Thread.currentThread().interrupt();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package visualiser.network;
|
||||
|
||||
import mock.exceptions.CommandConstructionException;
|
||||
import mock.model.commandFactory.Command;
|
||||
import network.Messages.AC35Data;
|
||||
import shared.model.RunnableWithFramePeriod;
|
||||
import visualiser.Commands.IncomingHeartBeatCommands.IncomingHeartBeatCommandFactory;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* The controller for heartbeat related messages, coming from the server to the client.
|
||||
*/
|
||||
public class IncomingHeartBeatController implements RunnableWithFramePeriod {
|
||||
|
||||
|
||||
/**
|
||||
* The incoming queue of messages to act on.
|
||||
*/
|
||||
private BlockingQueue<AC35Data> incomingMessages;
|
||||
|
||||
|
||||
/**
|
||||
* The heart beat service we are acting on.
|
||||
*/
|
||||
private IncomingHeartBeatService incomingHeartBeatService;
|
||||
|
||||
|
||||
/**
|
||||
* The queue to place commands on.
|
||||
*/
|
||||
private BlockingQueue<Command> outgoingCommands;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a {@link IncomingHeartBeatService} controller with the given parameters.
|
||||
* This accepts connection related messages, converts them to commands, and passes them to an outgoing command queue.
|
||||
* @param incomingMessages The message queue to read from.
|
||||
* @param incomingHeartBeatService The IncomingHeartBeatService (context) to act on.
|
||||
* @param outgoingCommands The queue to place outgoing commands on.
|
||||
*/
|
||||
public IncomingHeartBeatController(BlockingQueue<AC35Data> incomingMessages, IncomingHeartBeatService incomingHeartBeatService, BlockingQueue<Command> outgoingCommands) {
|
||||
this.incomingMessages = incomingMessages;
|
||||
this.incomingHeartBeatService = incomingHeartBeatService;
|
||||
this.outgoingCommands = outgoingCommands;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
while (!Thread.interrupted()) {
|
||||
|
||||
try {
|
||||
AC35Data message = incomingMessages.take();
|
||||
Command command = IncomingHeartBeatCommandFactory.create(message, incomingHeartBeatService);
|
||||
outgoingCommands.put(command);
|
||||
|
||||
} catch (CommandConstructionException e) {
|
||||
Logger.getGlobal().log(Level.WARNING, "IncomingHeartBeatController: " + this + " could not create command from message.", e);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Logger.getGlobal().log(Level.SEVERE, "IncomingHeartBeatController: " + this + " was interrupted on thread: " + Thread.currentThread(), e);
|
||||
Thread.currentThread().interrupt();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
package visualiser.network;
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import shared.model.RunnableWithFramePeriod;
|
||||
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
|
||||
/**
|
||||
* Tracks the heart beat status of a connection.
|
||||
*/
|
||||
public class IncomingHeartBeatService implements RunnableWithFramePeriod {
|
||||
|
||||
|
||||
/**
|
||||
* Timestamp of the last sent heartbeat message.
|
||||
*/
|
||||
private long lastHeartbeatTime;
|
||||
|
||||
|
||||
/**
|
||||
* Sequence number for heartbeat messages.
|
||||
*/
|
||||
private long lastHeartBeatSeqNum;
|
||||
|
||||
|
||||
/**
|
||||
* The incoming commands to execute.
|
||||
*/
|
||||
private BlockingQueue<Command> incomingCommands;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates an {@link IncomingHeartBeatService} which executes commands from a given queue.
|
||||
* @param incomingCommands Queue to read and execute commands from.
|
||||
*/
|
||||
public IncomingHeartBeatService(BlockingQueue<Command> incomingCommands) {
|
||||
this.incomingCommands = incomingCommands;
|
||||
|
||||
this.lastHeartbeatTime = System.currentTimeMillis();
|
||||
this.lastHeartBeatSeqNum = -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the last heart beat time to a given value.
|
||||
* @param lastHeartbeatTime Timestamp of heartbeat.
|
||||
*/
|
||||
public void setLastHeartbeatTime(long lastHeartbeatTime) {
|
||||
this.lastHeartbeatTime = lastHeartbeatTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the last heart beat sequence number to a given value.
|
||||
* @param lastHeartBeatSeqNum Sequence number of heartbeat.
|
||||
*/
|
||||
public void setLastHeartBeatSeqNum(long lastHeartBeatSeqNum) {
|
||||
this.lastHeartBeatSeqNum = lastHeartBeatSeqNum;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the time since last heartbeat, in milliseconds.
|
||||
*
|
||||
* @return Time since last heartbeat, in milliseconds..
|
||||
*/
|
||||
private long timeSinceHeartbeat() {
|
||||
long now = System.currentTimeMillis();
|
||||
return (now - lastHeartbeatTime);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether or not the heartBeat service considers the connection "alive".
|
||||
* Going 10,000ms without receiving a heartBeat means that the connection is "dead".
|
||||
* @return True if alive, false if dead.
|
||||
*/
|
||||
public boolean isAlive() {
|
||||
long heartBeatPeriod = 10000;
|
||||
|
||||
return (timeSinceHeartbeat() < heartBeatPeriod);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
|
||||
while (!Thread.interrupted()) {
|
||||
|
||||
try {
|
||||
Command command = incomingCommands.take();
|
||||
command.execute();
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Logger.getGlobal().log(Level.SEVERE, "IncomingHeartBeatService: " + this + " was interrupted on thread: " + Thread.currentThread() + " while reading command.", e);
|
||||
Thread.currentThread().interrupt();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,477 @@
|
||||
package visualiser.network;
|
||||
|
||||
|
||||
import mock.model.commandFactory.Command;
|
||||
import mock.model.commandFactory.CompositeCommand;
|
||||
import network.MessageRouters.MessageRouter;
|
||||
import network.Messages.AC35Data;
|
||||
import network.Messages.Enums.MessageType;
|
||||
import network.Messages.Enums.RequestToJoinEnum;
|
||||
import network.Messages.LatestMessages;
|
||||
import network.StreamRelated.MessageDeserialiser;
|
||||
import network.StreamRelated.MessageSerialiser;
|
||||
import shared.model.RunnableWithFramePeriod;
|
||||
import visualiser.model.VisualiserRaceEvent;
|
||||
import visualiser.model.VisualiserRaceController;
|
||||
import visualiser.enums.ConnectionToServerState;
|
||||
import visualiser.gameController.ControllerClient;
|
||||
import visualiser.model.VisualiserRaceState;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* This class handles the client-server connection handshake.
|
||||
*/
|
||||
public class ServerConnection implements RunnableWithFramePeriod {
|
||||
|
||||
/**
|
||||
* The socket for the connection to server.
|
||||
*/
|
||||
private Socket socket;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Used to send client input to server.
|
||||
*/
|
||||
private ControllerClient controllerClient;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Used to write messages to socket.
|
||||
*/
|
||||
private MessageSerialiser messageSerialiser;
|
||||
/**
|
||||
* The thread {@link #messageSerialiser} runs on.
|
||||
*/
|
||||
private Thread messageSerialiserThread;
|
||||
|
||||
/**
|
||||
* Used to read messages from socket.
|
||||
*/
|
||||
private MessageDeserialiser messageDeserialiser;
|
||||
/**
|
||||
* The thread {@link #messageDeserialiser} runs on.
|
||||
*/
|
||||
private Thread messageDeserialiserThread;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Router to route messages to correct queue.
|
||||
*/
|
||||
private MessageRouter messageRouter;
|
||||
/**
|
||||
* The thread {@link #messageRouter} runs on.
|
||||
*/
|
||||
private Thread messageRouterThread;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The state of the connection to the client.
|
||||
*/
|
||||
private ConnectionToServer connectionToServer;
|
||||
/**
|
||||
* The thread {@link #connectionToServer} runs on.
|
||||
*/
|
||||
private Thread connectionToServerThread;
|
||||
|
||||
/**
|
||||
* The controller which handles JoinAcceptance messages.
|
||||
*/
|
||||
private ConnectionToServerController connectionToServerController;
|
||||
/**
|
||||
* The thread {@link #connectionToServerController} runs on.
|
||||
*/
|
||||
private Thread connectionToServerControllerThread;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Tracks the heartBeat status of the connection.
|
||||
*/
|
||||
private IncomingHeartBeatService heartBeatService;
|
||||
/**
|
||||
* The thread {@link #heartBeatService} runs on.
|
||||
*/
|
||||
private Thread heartBeatServiceThread;
|
||||
|
||||
/**
|
||||
* Tracks the heartBeat status of the connection.
|
||||
*/
|
||||
private IncomingHeartBeatController heartBeatController;
|
||||
/**
|
||||
* The thread {@link #heartBeatController} runs on.
|
||||
*/
|
||||
private Thread heartBeatControllerThread;
|
||||
|
||||
|
||||
/**
|
||||
* This is the race we are modelling.
|
||||
*/
|
||||
private VisualiserRaceState visualiserRaceState;
|
||||
|
||||
/**
|
||||
* The CompositeCommand to place race commands in.
|
||||
*/
|
||||
private CompositeCommand raceCommands;
|
||||
|
||||
|
||||
/**
|
||||
* Used to convert incoming messages into a race snapshot.
|
||||
*/
|
||||
private VisualiserRaceController visualiserRaceController;
|
||||
/**
|
||||
* The thread {@link #visualiserRaceController} runs on.
|
||||
*/
|
||||
private Thread visualiserRaceControllerThread;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates a server connection, using a given socket.
|
||||
* @param socket The socket which connects to the client.
|
||||
* @param visualiserRaceState The race for the {@link VisualiserRaceController} to send commands to.
|
||||
* @param raceCommands The CompositeCommand to place race commands in.
|
||||
* @param requestType The type of join request to make.
|
||||
* @throws IOException Thrown if there is a problem with the client socket.
|
||||
*/
|
||||
public ServerConnection(Socket socket, VisualiserRaceState visualiserRaceState, CompositeCommand raceCommands, RequestToJoinEnum requestType) throws IOException {
|
||||
this.socket = socket;
|
||||
this.visualiserRaceState = visualiserRaceState;
|
||||
this.raceCommands = raceCommands;
|
||||
|
||||
createMessageSerialiser(socket);
|
||||
createMessageDeserialiser(socket);
|
||||
|
||||
createRouter(messageDeserialiser.getMessagesRead());
|
||||
|
||||
createConnectionToServer(requestType);
|
||||
|
||||
|
||||
messageRouterThread.start();
|
||||
|
||||
|
||||
this.controllerClient = new ControllerClient(messageRouter.getIncomingMessageQueue());
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Creates this connection's {@link MessageRouter}, and gives it a queue to read from.
|
||||
* Does not start {@link #messageRouterThread}. Start it after setting up any initial routes.
|
||||
* @param inputQueue Queue for the MessageRouter to read from.
|
||||
*/
|
||||
private void createRouter(BlockingQueue<AC35Data> inputQueue) {
|
||||
this.messageRouter = new MessageRouter(inputQueue);
|
||||
|
||||
this.messageRouterThread = new Thread(messageRouter, "ServerConnection()->MessageRouter thread " + messageRouter);
|
||||
|
||||
//Unrouted messages get sent back to the router. Kind of ugly, but we do this to ensure that no messages are lost while initializing (e.g., XML message being received before setting up the route for it).
|
||||
messageRouter.addDefaultRoute(messageRouter.getIncomingMessageQueue());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates the {@link #connectionToServer} and {@link #connectionToServerController}, and starts their threads.
|
||||
* @param requestType The type of join request to make to server.
|
||||
*/
|
||||
private void createConnectionToServer(RequestToJoinEnum requestType) {
|
||||
|
||||
//ConnectionToServer executes these commands.
|
||||
BlockingQueue<Command> commands = new LinkedBlockingQueue<>();
|
||||
this.connectionToServer = new ConnectionToServer(ConnectionToServerState.UNKNOWN, requestType, commands, messageRouter.getIncomingMessageQueue());
|
||||
|
||||
//ConnectionToServerController receives messages, and places commands on the above command queue.
|
||||
BlockingQueue<AC35Data> incomingJoinMessages = new LinkedBlockingQueue<>();
|
||||
this.connectionToServerController = new ConnectionToServerController(incomingJoinMessages, connectionToServer, commands);
|
||||
|
||||
//Route JoinAcceptance messages to the controller, and RequestToJoin to the socket.
|
||||
this.messageRouter.addRoute(MessageType.JOIN_ACCEPTANCE, incomingJoinMessages);
|
||||
this.messageRouter.addRoute(MessageType.REQUEST_TO_JOIN, messageSerialiser.getMessagesToSend());
|
||||
|
||||
|
||||
//Start the above on new threads.
|
||||
this.connectionToServerThread = new Thread(connectionToServer, "ServerConnection()->ConnectionToServer thread " + connectionToServer);
|
||||
this.connectionToServerThread.start();
|
||||
|
||||
this.connectionToServerControllerThread = new Thread(connectionToServerController,"ServerConnection()->ConnectionToServerController thread " + connectionToServerController);
|
||||
this.connectionToServerControllerThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes connection message related routes from the router.
|
||||
* This is called after the client-server connection is properly established, so that any future (erroneous) connection messages get ignored.
|
||||
*/
|
||||
private void removeConnectionRoutes() {
|
||||
this.messageRouter.removeRoute(MessageType.JOIN_ACCEPTANCE);
|
||||
this.messageRouter.removeRoute(MessageType.REQUEST_TO_JOIN);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates the {@link #messageSerialiser} and starts its thread.
|
||||
* @param socket The socket to write to.
|
||||
* @throws IOException Thrown if we cannot get an outputStream from the socket
|
||||
*/
|
||||
private void createMessageSerialiser(Socket socket) throws IOException {
|
||||
BlockingQueue<AC35Data> outputQueue = new LinkedBlockingQueue<>();
|
||||
this.messageSerialiser = new MessageSerialiser(socket.getOutputStream(), outputQueue);
|
||||
|
||||
this.messageSerialiserThread = new Thread(messageSerialiser, "ServerConnection()->MessageSerialiser thread " + messageSerialiser);
|
||||
this.messageSerialiserThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the {@link #messageDeserialiser} and starts its thread.
|
||||
* @param socket The socket to read from.
|
||||
* @throws IOException Thrown if we cannot get an inputStream from the socket
|
||||
*/
|
||||
private void createMessageDeserialiser(Socket socket) throws IOException {
|
||||
BlockingQueue<AC35Data> inputQueue = new LinkedBlockingQueue<>();
|
||||
this.messageDeserialiser = new MessageDeserialiser(socket.getInputStream(), inputQueue);
|
||||
|
||||
this.messageDeserialiserThread = new Thread(messageDeserialiser, "ServerConnection()->MessageDeserialiser thread " + messageDeserialiser);
|
||||
this.messageDeserialiserThread.start();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates the {@link #heartBeatService} and {@link #heartBeatController} and starts their threads.
|
||||
*/
|
||||
private void createHeartBeatService() {
|
||||
|
||||
//IncomingHeartBeatService executes these commands.
|
||||
BlockingQueue<Command> commands = new LinkedBlockingQueue<>();
|
||||
this.heartBeatService = new IncomingHeartBeatService(commands);
|
||||
|
||||
//IncomingHeartBeatController receives messages, and places commands on the above command queue.
|
||||
BlockingQueue<AC35Data> incomingHeartBeatMessages = new LinkedBlockingQueue<>();
|
||||
this.heartBeatController = new IncomingHeartBeatController(incomingHeartBeatMessages, heartBeatService, commands);
|
||||
|
||||
//Route HeartBeat messages to the controller.
|
||||
this.messageRouter.addRoute(MessageType.HEARTBEAT, incomingHeartBeatMessages);
|
||||
|
||||
|
||||
//Start the above on new threads.
|
||||
this.heartBeatServiceThread = new Thread(heartBeatService, "ServerConnection()->IncomingHeartBeatService thread " + connectionToServer);
|
||||
this.heartBeatServiceThread.start();
|
||||
|
||||
this.heartBeatControllerThread = new Thread(heartBeatController,"ServerConnection()->IncomingHeartBeatController thread " + connectionToServerController);
|
||||
this.heartBeatControllerThread.start();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates the {@link #visualiserRaceController} and starts its thread.
|
||||
*/
|
||||
private void createVisualiserRaceController() {
|
||||
|
||||
|
||||
//VisualiserRaceController receives messages, and places commands on the race's command queue.
|
||||
BlockingQueue<AC35Data> incomingMessages = new LinkedBlockingQueue<>();
|
||||
this.visualiserRaceController = new VisualiserRaceController(incomingMessages, visualiserRaceState, raceCommands);
|
||||
|
||||
|
||||
//Routes.
|
||||
this.messageRouter.addRoute(MessageType.BOATLOCATION, incomingMessages);
|
||||
this.messageRouter.addRoute(MessageType.RACESTATUS, incomingMessages);
|
||||
this.messageRouter.addRoute(MessageType.RACESTARTSTATUS, incomingMessages);
|
||||
this.messageRouter.addRoute(MessageType.AVGWIND, incomingMessages);
|
||||
this.messageRouter.addRoute(MessageType.COURSEWIND, incomingMessages);
|
||||
this.messageRouter.addRoute(MessageType.CHATTERTEXT, incomingMessages);
|
||||
this.messageRouter.addRoute(MessageType.DISPLAYTEXTMESSAGE, incomingMessages);
|
||||
this.messageRouter.addRoute(MessageType.YACHTACTIONCODE, incomingMessages);
|
||||
this.messageRouter.addRoute(MessageType.YACHTEVENTCODE, incomingMessages);
|
||||
this.messageRouter.addRoute(MessageType.MARKROUNDING, incomingMessages);
|
||||
this.messageRouter.addRoute(MessageType.XMLMESSAGE, incomingMessages);
|
||||
this.messageRouter.addRoute(MessageType.ASSIGN_PLAYER_BOAT, incomingMessages);
|
||||
this.messageRouter.removeDefaultRoute(); //We no longer want to keep un-routed messages.
|
||||
|
||||
|
||||
//Start the above on a new thread.
|
||||
|
||||
this.visualiserRaceControllerThread = new Thread(visualiserRaceController, "ServerConnection()->VisualiserRaceController thread " + visualiserRaceController);
|
||||
this.visualiserRaceControllerThread.start();
|
||||
|
||||
}
|
||||
|
||||
|
||||
//TODO create input controller here. RaceController should query for it, if it exists.
|
||||
private void createPlayerInputController() {
|
||||
|
||||
this.messageRouter.addRoute(MessageType.BOATACTION, messageSerialiser.getMessagesToSend());
|
||||
//TODO routes
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
//Monitor the connection state.
|
||||
|
||||
long previousFrameTime = System.currentTimeMillis();
|
||||
|
||||
while (!Thread.interrupted()) {
|
||||
|
||||
long currentFrameTime = System.currentTimeMillis();
|
||||
waitForFramePeriod(previousFrameTime, currentFrameTime, 100);
|
||||
previousFrameTime = currentFrameTime;
|
||||
|
||||
|
||||
ConnectionToServerState state = connectionToServer.getConnectionState();
|
||||
|
||||
switch (state) {
|
||||
|
||||
case CONNECTED:
|
||||
connected();
|
||||
break;
|
||||
|
||||
case DECLINED:
|
||||
declined();
|
||||
break;
|
||||
|
||||
case TIMED_OUT:
|
||||
timedOut();
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#CONNECTED}.
|
||||
*/
|
||||
private void connected() {
|
||||
|
||||
createHeartBeatService();
|
||||
|
||||
createVisualiserRaceController();
|
||||
|
||||
if (connectionToServer.getRequestType() == RequestToJoinEnum.PARTICIPANT) {
|
||||
createPlayerInputController();
|
||||
}
|
||||
|
||||
|
||||
//We no longer want connection messages to be accepted.
|
||||
removeConnectionRoutes();
|
||||
|
||||
|
||||
//We interrupt as this thread's run() isn't needed anymore.
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#DECLINED}.
|
||||
*/
|
||||
private void declined() {
|
||||
Logger.getGlobal().log(Level.WARNING, "Server handshake failed. Connection was declined.");
|
||||
|
||||
terminate();
|
||||
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Called when the {@link #connectionToServer} state changes to {@link ConnectionToServerState#TIMED_OUT}.
|
||||
*/
|
||||
private void timedOut() {
|
||||
Logger.getGlobal().log(Level.WARNING, "Server handshake failed. Connection timed out.");
|
||||
|
||||
terminate();
|
||||
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Determines whether or not this connection is still alive.
|
||||
* This is based off whether the {@link #messageDeserialiser}, {@link #messageSerialiser}, and {@link #heartBeatService} are alive.
|
||||
* @return True if it is alive, false otherwise.
|
||||
*/
|
||||
public boolean isAlive() {
|
||||
return messageDeserialiser.isRunning() && messageSerialiser.isRunning() && heartBeatService.isAlive();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the controller client, which writes BoatAction messages to the outgoing queue.
|
||||
* @return The ControllerClient.
|
||||
*/
|
||||
public ControllerClient getControllerClient() {
|
||||
return controllerClient;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Terminates the connection and any running threads.
|
||||
*/
|
||||
public void terminate() {
|
||||
|
||||
if (this.messageRouterThread != null) {
|
||||
this.messageRouterThread.interrupt();
|
||||
}
|
||||
|
||||
|
||||
if (this.messageSerialiserThread != null) {
|
||||
this.messageSerialiserThread.interrupt();
|
||||
}
|
||||
if (this.messageDeserialiserThread != null) {
|
||||
this.messageDeserialiserThread.interrupt();
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (this.connectionToServerThread != null) {
|
||||
this.connectionToServerThread.interrupt();
|
||||
}
|
||||
if (this.connectionToServerControllerThread != null) {
|
||||
this.connectionToServerControllerThread.interrupt();
|
||||
}
|
||||
|
||||
|
||||
if (this.heartBeatServiceThread != null) {
|
||||
this.heartBeatServiceThread.interrupt();
|
||||
}
|
||||
if (this.heartBeatControllerThread != null) {
|
||||
this.heartBeatControllerThread.interrupt();
|
||||
}
|
||||
|
||||
|
||||
if (this.visualiserRaceControllerThread != null) {
|
||||
this.visualiserRaceControllerThread.interrupt();
|
||||
}
|
||||
|
||||
|
||||
|
||||
//TODO input controller?
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<Race>
|
||||
<RaceID>5326</RaceID>
|
||||
<RaceType>FLEET</RaceType>
|
||||
<CreationTimeDate>CREATION_TIME</CreationTimeDate>
|
||||
<RaceStartTime Postpone="false" Time="START_TIME"/>
|
||||
<Participants>
|
||||
<Yacht SourceID="126"/>
|
||||
</Participants>
|
||||
<CompoundMarkSequence>
|
||||
<Corner SeqID="1" CompoundMarkID="1" Rounding="SP" ZoneSize="3" />
|
||||
<Corner SeqID="2" CompoundMarkID="2" Rounding="Port" ZoneSize="3" />
|
||||
<Corner SeqID="3" CompoundMarkID="4" Rounding="Port" ZoneSize="3" />
|
||||
<Corner SeqID="4" CompoundMarkID="3" Rounding="Starboard" ZoneSize="3" />
|
||||
<Corner SeqID="5" CompoundMarkID="4" Rounding="Port" ZoneSize="3" />
|
||||
<Corner SeqID="6" CompoundMarkID="5" Rounding="SP" ZoneSize="3"/>
|
||||
</CompoundMarkSequence>
|
||||
<Course>
|
||||
<CompoundMark CompoundMarkID="1" Name="Start Line">
|
||||
<Mark SeqId="1" Name="PRO" TargetLat="32.296577" TargetLng="-64.854304" SourceID="101"/>
|
||||
<Mark SeqId="2" Name="PIN" TargetLat="32.293771" TargetLng="-64.855242" SourceID="102"/>
|
||||
</CompoundMark>
|
||||
<CompoundMark CompoundMarkID="2" Name="Marker 1">
|
||||
<Mark Name="Marker1" TargetLat="32.293039" TargetLng="-64.843983" SourceID="103"/>
|
||||
</CompoundMark>
|
||||
<CompoundMark CompoundMarkID="3" Name="Windward Gate">
|
||||
<Mark Name="WGL" SeqId="1" TargetLat="32.28468" TargetLng="-64.850045" SourceID="104"/>
|
||||
<Mark Name="WGR" SeqId="2" TargetLat="32.280164" TargetLng="-64.847591" SourceID="105"/>
|
||||
</CompoundMark>
|
||||
<CompoundMark CompoundMarkID="4" Name="Leeward Gate">
|
||||
<Mark Name="LGL" SeqId="1" TargetLat="32.309693" TargetLng="-64.835249" SourceID="106"/>
|
||||
<Mark Name="LGR" SeqId="2" TargetLat="32.308046" TargetLng="-64.831785" SourceID="107"/>
|
||||
</CompoundMark>
|
||||
<CompoundMark CompoundMarkID="5" Name="Finish Line">
|
||||
<Mark Name="FL" SeqId="1" TargetLat="32.317379" TargetLng="-64.839291" SourceID="108"/>
|
||||
<Mark Name="FR" SeqId="2" TargetLat="32.317257" TargetLng="-64.83626" SourceID="109"/>
|
||||
</CompoundMark>
|
||||
</Course>
|
||||
<CourseLimit>
|
||||
<Limit Lat="32.313922" Lon="-64.837168" SeqID="1"/>
|
||||
<Limit Lat="32.317379" Lon="-64.839291" SeqID="2"/>
|
||||
<Limit Lat="32.317911" Lon="-64.836996" SeqID="3"/>
|
||||
<Limit Lat="32.317257" Lon="-64.83626" SeqID="4"/>
|
||||
<Limit Lat="32.304273" Lon="-64.822834" SeqID="5"/>
|
||||
<Limit Lat="32.279097" Lon="-64.841545" SeqID="6"/>
|
||||
<Limit Lat="32.279604" Lon="-64.849871" SeqID="7"/>
|
||||
<Limit Lat="32.289545" Lon="-64.854162" SeqID="8"/>
|
||||
<Limit Lat="32.290198" Lon="-64.858711" SeqID="9"/>
|
||||
<Limit Lat="32.297164" Lon="-64.856394" SeqID="10"/>
|
||||
<Limit Lat="32.296148" Lon="-64.849184" SeqID="11"/>
|
||||
</CourseLimit>
|
||||
</Race>
|
||||
@ -0,0 +1,72 @@
|
||||
package mock.model;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import shared.model.Bearing;
|
||||
import shared.model.Wind;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
|
||||
/**
|
||||
* Tests the {@link ConstantWindGenerator}.
|
||||
*/
|
||||
public class ConstantWindGeneratorTest {
|
||||
|
||||
WindGenerator windGenerator;
|
||||
Bearing windBearing;
|
||||
double windSpeedKnots;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
windBearing = Bearing.fromDegrees(78.5);
|
||||
windSpeedKnots = 18.54;
|
||||
|
||||
windGenerator = new ConstantWindGenerator(windBearing, windSpeedKnots);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests if the {@link WindGenerator#generateBaselineWind()} function works.
|
||||
*/
|
||||
@Test
|
||||
public void generateBaselineTest() {
|
||||
|
||||
int repetitions = 100;
|
||||
|
||||
for (int i = 0; i < repetitions; i++) {
|
||||
|
||||
Wind wind = windGenerator.generateBaselineWind();
|
||||
|
||||
assertEquals(windBearing.degrees(), wind.getWindDirection().degrees(), 0.01);
|
||||
assertEquals(windSpeedKnots, wind.getWindSpeed(), 0.01);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests if the {@link WindGenerator#generateNextWind(Wind)} ()} function works.
|
||||
*/
|
||||
@Test
|
||||
public void generateNextWindTest() {
|
||||
|
||||
int repetitions = 100;
|
||||
|
||||
Wind wind = windGenerator.generateBaselineWind();
|
||||
|
||||
for (int i = 0; i < repetitions; i++) {
|
||||
|
||||
wind = windGenerator.generateNextWind(wind);
|
||||
|
||||
assertEquals(windBearing.degrees(), wind.getWindDirection().degrees(), 0.01);
|
||||
assertEquals(windSpeedKnots, wind.getWindSpeed(), 0.01);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,7 +1,43 @@
|
||||
package mock.model;
|
||||
|
||||
import mock.dataInput.PolarParserTest;
|
||||
import network.Messages.LatestMessages;
|
||||
import shared.dataInput.*;
|
||||
import shared.exceptions.InvalidBoatDataException;
|
||||
import shared.exceptions.InvalidRaceDataException;
|
||||
import shared.exceptions.InvalidRegattaDataException;
|
||||
import shared.model.Bearing;
|
||||
import shared.model.Constants;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class MockRaceTest {
|
||||
//TODO
|
||||
|
||||
|
||||
/**
|
||||
* Creates a MockRace for use in testing.
|
||||
* Has a constant wind generator.
|
||||
* @return MockRace for testing.
|
||||
* @throws InvalidBoatDataException If the BoatDataSource cannot be created.
|
||||
* @throws InvalidRaceDataException If the RaceDataSource cannot be created.
|
||||
* @throws InvalidRegattaDataException If the RegattaDataSource cannot be created.
|
||||
*/
|
||||
public static MockRace createMockRace() throws InvalidBoatDataException, InvalidRaceDataException, InvalidRegattaDataException {
|
||||
|
||||
BoatDataSource boatDataSource = BoatXMLReaderTest.createBoatDataSource();
|
||||
RaceDataSource raceDataSource = RaceXMLReaderTest.createRaceDataSource();
|
||||
RegattaDataSource regattaDataSource = RegattaXMLReaderTest.createRegattaDataSource();
|
||||
|
||||
|
||||
Polars polars = PolarParserTest.createPolars();
|
||||
|
||||
WindGenerator windGenerator = new ConstantWindGenerator(Bearing.fromDegrees(230), 10);
|
||||
|
||||
MockRace mockRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, polars, Constants.RaceTimeScale, windGenerator);
|
||||
|
||||
|
||||
return mockRace;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,126 @@
|
||||
package mock.model;
|
||||
|
||||
import mock.exceptions.SourceIDAllocationException;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
|
||||
/**
|
||||
* Tests if allocating source IDs works.
|
||||
*/
|
||||
public class SourceIdAllocatorTest {
|
||||
|
||||
/**
|
||||
* This is the list of source IDs that we start with.
|
||||
*/
|
||||
private List<Integer> originalSourceIDs;
|
||||
|
||||
/**
|
||||
* Used to allocate source IDs.
|
||||
*/
|
||||
private SourceIdAllocator sourceIdAllocator;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
originalSourceIDs = new ArrayList<>();
|
||||
originalSourceIDs.add(120);
|
||||
originalSourceIDs.add(121);
|
||||
originalSourceIDs.add(122);
|
||||
originalSourceIDs.add(123);
|
||||
originalSourceIDs.add(124);
|
||||
originalSourceIDs.add(125);
|
||||
|
||||
|
||||
sourceIdAllocator = new SourceIdAllocator(originalSourceIDs);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests that allocation fails when we don't have any source IDs to allocate.
|
||||
*/
|
||||
@Test
|
||||
public void emptyAllocationTest() {
|
||||
|
||||
SourceIdAllocator allocator = new SourceIdAllocator(new ArrayList<>());
|
||||
|
||||
|
||||
try {
|
||||
int sourceID = allocator.allocateSourceID();
|
||||
|
||||
fail("Exception should have been thrown, but wasn't.");
|
||||
|
||||
} catch (SourceIDAllocationException e) {
|
||||
|
||||
//We expect this exception to be thrown - success.
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests that we can allocate a source ID.
|
||||
* @throws Exception Thrown in case of error.
|
||||
*/
|
||||
@Test
|
||||
public void allocationTest() throws Exception {
|
||||
|
||||
|
||||
int sourceID = sourceIdAllocator.allocateSourceID();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests that we can allocate source IDs, but it will eventually be unable to allocate source IDs.
|
||||
*/
|
||||
@Test
|
||||
public void allocationEventuallyFailsTest() {
|
||||
|
||||
while (true) {
|
||||
|
||||
try {
|
||||
int sourceID = sourceIdAllocator.allocateSourceID();
|
||||
|
||||
} catch (SourceIDAllocationException e) {
|
||||
//We expect to encounter this exception after enough allocations - success.
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests if we can allocate a source ID, return it, and reallocate it.
|
||||
* @throws Exception Thrown in case of error.
|
||||
*/
|
||||
@Test
|
||||
public void reallocationTest() throws Exception {
|
||||
|
||||
List<Integer> sourceIDList = new ArrayList<>();
|
||||
sourceIDList.add(123);
|
||||
|
||||
SourceIdAllocator sourceIdAllocator = new SourceIdAllocator(sourceIDList);
|
||||
|
||||
//Allocate.
|
||||
int sourceID = sourceIdAllocator.allocateSourceID();
|
||||
|
||||
//Return.
|
||||
sourceIdAllocator.returnSourceID(sourceID);
|
||||
|
||||
//Reallocate.
|
||||
int sourceID2 = sourceIdAllocator.allocateSourceID();
|
||||
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue