Merge branch 'master' into story_61

Also got client-server handshake working. Multiple clients can connect and control their own boat.
The client's boat has a larger black triangle drawn around it. Probably needs tidying up.
Added isClientBoat boolean to VisualiserBoat.

WindGenerator is now an interface, implemented by ConstantWindGenerator (useful for testing) and RandomWindGenerator. Added ConstantWindGeneratorTest.
RandomWindGenerator was formerly WindGenerator.
The WindGenerator is passed in to MockRace.

CommandFactory throws a CommandConstructionException if it cannot create a command.

MessageSerialiser and MessageDeserialiser correctly terminate on error.

Readded VisualiserInput's switch statement in the main loop, pending a refactor.

Removed the sleep statement from LobbyController - it was blocking javaFX thread.

Lobby.fxml: moved buttons and text input into grid panes, so they don't break when you resize screen.

Added some test object creation functions in several test classes (MockRaceTest.createMockRace(), BoatXMLReaderTest.createBoatDataSource(), etc...).

#story[1095]
main
fjc40 8 years ago
commit 19984772c0

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

@ -1,12 +1,11 @@
package mock.app;
import mock.enums.ConnectionStateEnum;
import mock.model.RaceLogic;
import mock.model.ClientConnection;
import mock.model.SourceIdAllocator;
import mock.model.commandFactory.CompositeCommand;
import network.Messages.Enums.XMLMessageType;
import network.Messages.LatestMessages;
import network.Messages.RaceSnapshot;
import network.Messages.XMLMessage;
import java.io.IOException;
@ -65,19 +64,23 @@ public class ConnectionAcceptor implements Runnable {
private short boatXMLSequenceNumber;
//regatta xml sequence number
private short regattaXMLSequenceNumber;
//
private RaceLogic raceLogic = null;
/**
* Connection Acceptor Constructor
* @param latestMessages Latest messages to be sent
* @param compositeCommand Collection of commands for race to execute.
* @param sourceIdAllocator Object used to allocate source IDs for clients.
* @param raceLogic The race the client will connect to.
* @throws IOException if a server socket cannot be instantiated.
*/
public ConnectionAcceptor(LatestMessages latestMessages, CompositeCommand compositeCommand, SourceIdAllocator sourceIdAllocator) throws IOException {
public ConnectionAcceptor(LatestMessages latestMessages, CompositeCommand compositeCommand, SourceIdAllocator sourceIdAllocator, RaceLogic raceLogic) throws IOException {
this.latestMessages = latestMessages;
this.compositeCommand = compositeCommand;
this.sourceIdAllocator = sourceIdAllocator;
this.raceLogic = raceLogic;
this.serverSocket = new ServerSocket(serverPort);
CheckClientConnection checkClientConnection = new CheckClientConnection(clientConnections);
@ -93,6 +96,8 @@ public class ConnectionAcceptor implements Runnable {
return serverPort;
}
/**
* Run the Acceptor
*/
@ -103,11 +108,13 @@ public class ConnectionAcceptor implements Runnable {
try {
System.out.println("Waiting for a connection...");//TEMP DEBUG REMOVE
Socket mockSocket = serverSocket.accept();
ClientConnection clientConnection = new ClientConnection(mockSocket, sourceIdAllocator, latestMessages, compositeCommand);
Logger.getGlobal().log(Level.INFO, String.format("Client connected. client ip/port = %s. Local ip/port = %s.", mockSocket.getRemoteSocketAddress(), mockSocket.getLocalSocketAddress()));
ClientConnection clientConnection = new ClientConnection(mockSocket, sourceIdAllocator, latestMessages, compositeCommand, raceLogic);
clientConnections.add(clientConnection);
@ -153,6 +160,7 @@ public class ConnectionAcceptor implements Runnable {
for (ClientConnection client : clientConnections) {
if (!client.isAlive()) {
connections.remove(client);
client.terminate();
Logger.getGlobal().log(Level.WARNING, "CheckClientConnection is removing the dead connection: " + client);
}

@ -2,10 +2,7 @@ package mock.app;
import mock.dataInput.PolarParser;
import mock.exceptions.EventConstructionException;
import mock.model.MockRace;
import mock.model.Polars;
import mock.model.RaceLogic;
import mock.model.SourceIdAllocator;
import mock.model.*;
import mock.model.commandFactory.CompositeCommand;
import network.Messages.LatestMessages;
import network.Messages.RaceSnapshot;
@ -15,6 +12,7 @@ import shared.exceptions.InvalidBoatDataException;
import shared.exceptions.InvalidRaceDataException;
import shared.exceptions.InvalidRegattaDataException;
import shared.exceptions.XMLReaderException;
import shared.model.Bearing;
import shared.model.Constants;
import javax.xml.transform.TransformerException;
@ -67,15 +65,27 @@ public class Event {
/**
* Constructs an event, using various XML files.
* @param singlePlayer Whether or not to create a single player event.
* @throws EventConstructionException Thrown if we cannot create an Event for any reason.
*/
public Event() throws EventConstructionException {
public Event(boolean singlePlayer) throws EventConstructionException {
singlePlayer = false;
String raceXMLFile = "mock/mockXML/raceTest.xml";
String boatsXMLFile = "mock/mockXML/boatTest.xml";
String regattaXMLFile = "mock/mockXML/regattaTest.xml";
if (singlePlayer) {
raceXMLFile = "mock/mockXML/raceSinglePlayer.xml";
boatsXMLFile = "mock/mockXML/boatsSinglePlayer.xml";
}
//Read XML files.
try {
this.raceXML = getRaceXMLAtCurrentTime(XMLReader.readXMLFileToString("mock/mockXML/raceTest.xml", StandardCharsets.UTF_8));
this.boatXML = XMLReader.readXMLFileToString("mock/mockXML/boatsSinglePlayer.xml", StandardCharsets.UTF_8);
this.regattaXML = XMLReader.readXMLFileToString("mock/mockXML/regattaTest.xml", StandardCharsets.UTF_8);
this.raceXML = getRaceXMLAtCurrentTime(XMLReader.readXMLFileToString(raceXMLFile, StandardCharsets.UTF_8));
this.boatXML = XMLReader.readXMLFileToString(boatsXMLFile, StandardCharsets.UTF_8);
this.regattaXML = XMLReader.readXMLFileToString(regattaXMLFile, StandardCharsets.UTF_8);
} catch (TransformerException | XMLReaderException e) {
throw new EventConstructionException("Could not read XML files.", e);
@ -98,17 +108,48 @@ public class Event {
}
//Create connection acceptor.
this.sourceIdAllocator = new SourceIdAllocator(raceDataSource.getParticipants());
this.compositeCommand = new CompositeCommand();
this.latestMessages = new LatestMessages();
//Create and start race.
WindGenerator windGenerator = new RandomWindGenerator(
Bearing.fromDegrees(225),
Bearing.fromDegrees(215),
Bearing.fromDegrees(235),
12d,
8d,
16d );
RaceLogic newRace = new RaceLogic(
new MockRace(
boatDataSource,
raceDataSource,
regattaDataSource,
this.latestMessages,
this.boatPolars,
Constants.RaceTimeScale,
windGenerator ),
this.latestMessages,
this.compositeCommand);
new Thread(newRace, "Event.Start()->RaceLogic thread").start();
//Create connection acceptor.
try {
this.connectionAcceptor = new ConnectionAcceptor(latestMessages, compositeCommand, sourceIdAllocator);
this.connectionAcceptor = new ConnectionAcceptor(latestMessages, compositeCommand, sourceIdAllocator, newRace);
} catch (IOException e) {
throw new EventConstructionException("Could not create ConnectionAcceptor.", e);
}
new Thread(connectionAcceptor, "Event.Start()->ConnectionAcceptor thread").start();
sendXMLs();
}
@ -125,16 +166,8 @@ public class Event {
* Sends the initial race data and then begins race simulation.
*/
public void start() {
new Thread(connectionAcceptor, "Event.Start()->ConnectionAcceptor thread").start();
sendXMLs();
//Create and start race.
RaceLogic newRace = new RaceLogic(new MockRace(boatDataSource, raceDataSource, regattaDataSource, this.latestMessages, this.boatPolars, Constants.RaceTimeScale), this.latestMessages, this.compositeCommand);
new Thread(newRace, "Event.Start()->RaceLogic thread").start();
}
/**

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

@ -36,6 +36,11 @@ public class ClientConnection implements Runnable {
*/
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.
@ -53,16 +58,31 @@ public class ClientConnection implements Runnable {
*/
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.
@ -100,13 +120,15 @@ public class ClientConnection implements Runnable {
* @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) throws IOException {
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<>();
@ -120,7 +142,8 @@ public class ClientConnection implements Runnable {
this.heartBeatService = new HeartBeatService(outputQueue);
new Thread(heartBeatService, "ClientConnection()->HeartBeatService thread " + heartBeatService).start();
this.heartBeatThread = new Thread(heartBeatService, "ClientConnection()->HeartBeatService thread " + heartBeatService);
this.heartBeatThread.start();
}
@ -145,7 +168,7 @@ public class ClientConnection implements Runnable {
* @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 {
private void handshake() throws SourceIDAllocationException, HandshakeException {
//This function is a bit messy, and could probably be refactored a bit.
@ -162,16 +185,20 @@ public class ClientConnection implements Runnable {
allocatedSourceID = sourceIdAllocator.allocateSourceID();
this.controllerServer = new ControllerServer(compositeCommand, inputQueue, allocatedSourceID);
new Thread(controllerServer, "ClientConnection.run()->ControllerServer thread" + controllerServer).start();
this.controllerServer = new ControllerServer(compositeCommand, inputQueue, allocatedSourceID, raceLogic.getRace());
this.controllerServerThread = new Thread(controllerServer, "ClientConnection.run()->ControllerServer thread" + controllerServer);
this.controllerServerThread.start();
}
this.mockOutput = new MockOutput(latestMessages, outputQueue);
new Thread(mockOutput, "ClientConnection.run()->MockOutput thread" + mockOutput).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;
}
@ -240,4 +267,22 @@ public class ClientConnection implements Runnable {
}
/**
* 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();
}
}

@ -25,7 +25,7 @@ public class MockBoat extends Boat {
/**
* Stores whether the boat is on autoVMG or not
*/
private boolean autoVMG = true;
private boolean autoVMG = false;

@ -2,7 +2,6 @@ package mock.model;
import network.Messages.Enums.BoatStatusEnum;
import network.Messages.LatestMessages;
import org.opengis.geometry.primitive.*;
import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import network.Messages.Enums.RaceStatusEnum;
@ -13,7 +12,6 @@ import shared.model.Bearing;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.*;
import static java.lang.Math.cos;
@ -59,7 +57,7 @@ public class MockRace extends Race {
* @param polars The polars table to be used for boat simulation.
* @param timeScale The timeScale for the race. See {@link Constants#RaceTimeScale}.
*/
public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale) {
public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale, WindGenerator windGenerator) {
super(boatDataSource, raceDataSource, regattaDataSource, latestMessages);
@ -69,14 +67,8 @@ public class MockRace extends Race {
this.shrinkBoundary = GPSCoordinate.getShrinkBoundary(this.boundary);
//Set up wind generator. It may be tidier to create this outside the race (with the values sourced from a data file maybe?) and pass it in.
this.windGenerator = new WindGenerator(
Bearing.fromDegrees(225),
Bearing.fromDegrees(215),
Bearing.fromDegrees(235),
12d,
8d,
16d );
this.windGenerator = windGenerator;
//Wind.
this.setWind(windGenerator.generateBaselineWind());

@ -2,14 +2,15 @@ package mock.model;
import javafx.animation.AnimationTimer;
import mock.model.commandFactory.CompositeCommand;
import mock.model.commandFactory.CommandFactory;
import network.Messages.Enums.BoatActionEnum;
import network.Messages.Enums.BoatStatusEnum;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.LatestMessages;
import shared.model.RunnableWithFramePeriod;
public class RaceLogic implements Runnable {
public class RaceLogic implements RunnableWithFramePeriod {
/**
* State of current race modified by this object
*/
@ -66,6 +67,7 @@ public class RaceLogic implements Runnable {
//Parse the race snapshot.
server.parseSnapshot();
// Change wind direction
race.changeWindDirection();
@ -113,7 +115,7 @@ public class RaceLogic implements Runnable {
currentTime = System.currentTimeMillis();
//Execute commands from clients.
commands.execute(race);
commands.execute();
//Update race time.
race.updateRaceTime(currentTime);
@ -172,4 +174,12 @@ public class RaceLogic implements Runnable {
}
};
/**
* Returns the race state that this RaceLogic is simulating.
* @return Race state this RaceLogic is simulating.
*/
public MockRace getRace() {
return race;
}
}

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

@ -1,249 +1,29 @@
package mock.model;
import shared.model.Bearing;
import shared.model.Wind;
import java.util.Random;
/**
* This class generates Wind objects for use in a MockRace.
* Bounds on bearing and speed can be specified.
* Wind can be completely random, or random incremental change.
* Interface for wind generators. It allows for generating a baseline wind, and subsequent winds.
*/
public class WindGenerator {
/**
* The bearing the wind direction starts at.
*/
private Bearing windBaselineBearing;
/**
* The lower bearing angle that the wind may have.
*/
private Bearing windBearingLowerBound;
/**
* The upper bearing angle that the wind may have.
*/
private Bearing windBearingUpperBound;
/**
* The speed the wind starts at, in knots.
*/
private double windBaselineSpeed;
/**
* The lower speed that the wind may have, in knots.
*/
private double windSpeedLowerBound;
/**
* The upper speed that the wind may have, in knots.
*/
private double windSpeedUpperBound;
/**
* Creates a wind generator, with a baseline, lower bound, and upper bound, for the wind speed and direction.
* @param windBaselineBearing Baseline wind direction.
* @param windBearingLowerBound Lower bound for wind direction.
* @param windBearingUpperBound Upper bound for wind direction.
* @param windBaselineSpeed Baseline wind speed, in knots.
* @param windSpeedLowerBound Lower bound for wind speed, in knots.
* @param windSpeedUpperBound Upper bound for wind speed, in knots.
*/
public WindGenerator(Bearing windBaselineBearing, Bearing windBearingLowerBound, Bearing windBearingUpperBound, double windBaselineSpeed, double windSpeedLowerBound, double windSpeedUpperBound) {
this.windBaselineBearing = windBaselineBearing;
this.windBearingLowerBound = windBearingLowerBound;
this.windBearingUpperBound = windBearingUpperBound;
this.windBaselineSpeed = windBaselineSpeed;
this.windSpeedLowerBound = windSpeedLowerBound;
this.windSpeedUpperBound = windSpeedUpperBound;
public interface WindGenerator {
}
/**
* Generates a wind object using the baseline wind speed and bearing.
* @return Baseline wind object.
*/
public Wind generateBaselineWind() {
return new Wind(windBaselineBearing, windBaselineSpeed);
}
/**
* Generates a random Wind object, that is within the provided bounds.
* @return Generated wind object.
*/
public Wind generateRandomWind() {
double windSpeed = generateRandomWindSpeed();
Bearing windBearing = generateRandomWindBearing();
return new Wind(windBearing, windSpeed);
}
/**
* Generates a random wind speed within the specified bounds. In knots.
* @return Wind speed, in knots.
*/
private double generateRandomWindSpeed() {
double randomSpeedKnots = generateRandomValueInBounds(windSpeedLowerBound, windSpeedUpperBound);
return randomSpeedKnots;
}
/**
* Generates a random wind bearing within the specified bounds.
* @return Wind bearing.
*/
private Bearing generateRandomWindBearing() {
double randomBearingDegrees = generateRandomValueInBounds(windBearingLowerBound.degrees(), windBearingUpperBound.degrees());
return Bearing.fromDegrees(randomBearingDegrees);
}
/**
* Generates a random value within a specified interval.
* @param lowerBound The lower bound of the interval.
* @param upperBound The upper bound of the interval.
* @return A random value within the interval.
*/
private static double generateRandomValueInBounds(double lowerBound, double upperBound) {
Wind generateBaselineWind();
float proportion = new Random().nextFloat();
double delta = upperBound - lowerBound;
double amount = delta * proportion;
double finalAmount = amount + lowerBound;
return finalAmount;
}
/**
* Generates a new value within an interval, given a start value, chance to change, and change amount.
* @param lowerBound Lower bound of interval.
* @param upperBound Upper bound of interval.
* @param currentValue The current value to change.
* @param changeAmount The amount to change by.
* @param chanceToChange The change to actually change the value.
* @return The new value.
*/
private static double generateNextValueInBounds(double lowerBound, double upperBound, double currentValue, double changeAmount, double chanceToChange) {
float chance = new Random().nextFloat();
if (chance <= chanceToChange) {
currentValue += changeAmount;
} else if (chance <= (2 * chanceToChange)) {
currentValue -= changeAmount;
}
currentValue = clamp(lowerBound, upperBound, currentValue);
return currentValue;
}
/**
* Generates the next Wind object, that is within the provided bounds. This randomly increases or decreases the wind's speed and bearing.
* Generates the next Wind object, according to the implementation of the wind generator.
* @param currentWind The current wind to change. This is not modified.
* @return Generated wind object.
*/
public Wind generateNextWind(Wind currentWind) {
double windSpeed = generateNextWindSpeed(currentWind.getWindSpeed());
Bearing windBearing = generateNextWindBearing(currentWind.getWindDirection());
return new Wind(windBearing, windSpeed);
}
/**
* Generates the next wind speed to use.
* @param windSpeed Current wind speed, in knots.
* @return Next wind speed, in knots.
*/
private double generateNextWindSpeed(double windSpeed) {
double chanceToChange = 0.2;
double changeAmount = 0.1;
double nextWindSpeed = generateNextValueInBounds(
windSpeedLowerBound,
windSpeedUpperBound,
windSpeed,
changeAmount,
chanceToChange);
return nextWindSpeed;
}
/**
* Generates the next wind speed to use.
* @param windBearing Current wind bearing.
* @return Next wind speed.
*/
private Bearing generateNextWindBearing(Bearing windBearing) {
double chanceToChange = 0.2;
double changeAmount = 0.5;
double nextWindBearingDegrees = generateNextValueInBounds(
windBearingLowerBound.degrees(),
windBearingUpperBound.degrees(),
windBearing.degrees(),
changeAmount,
chanceToChange);
return Bearing.fromDegrees(nextWindBearingDegrees);
}
/**
* Clamps a value to be within an interval.
* @param lower Lower bound of the interval.
* @param upper Upper bound of the interval.
* @param value Value to clamp.
* @return The clamped value.
*/
private static double clamp(double lower, double upper, double value) {
if (value > upper) {
value = upper;
} else if (value < lower) {
value = lower;
}
return value;
}
Wind generateNextWind(Wind currentWind);
}

@ -1,8 +1,10 @@
package mock.model.commandFactory;
import mock.exceptions.CommandConstructionException;
import mock.model.MockBoat;
import mock.model.MockRace;
import network.Messages.Enums.BoatActionEnum;
import network.Messages.BoatAction;
import shared.exceptions.BoatNotFoundException;
/**
* Factory class for Command objects
@ -11,15 +13,28 @@ public class CommandFactory {
/**
* Generates a command on a race and boat corresponding to the protocol action number.
* @param race to receive command
* @param boat to receive command in race
* @param action number to select command
* @return
* @return The command to execute the given action.
* @throws CommandConstructionException Thrown if the command cannot be constructed (e.g., unknown action type).
*/
public static Command createCommand(MockRace race, MockBoat boat, BoatActionEnum action) {
switch(action) {
public static Command createCommand(MockRace race, BoatAction action) throws CommandConstructionException {
MockBoat boat = null;
try {
boat = race.getBoat(action.getSourceID());
} catch (BoatNotFoundException e) {
throw new CommandConstructionException("Could not create command for BoatAction: " + action + ". Boat with sourceID: " + action.getSourceID() + " not found.", e);
}
switch(action.getBoatAction()) {
case AUTO_PILOT: return new VMGCommand(race, boat);
case TACK_GYBE: return new TackGybeCommand(race, boat);
default: return null; // TODO - please please have discussion over what to default to
case UPWIND: return new WindCommand(race, boat, true);
case DOWNWIND: return new WindCommand(race, boat, false);
default: throw new CommandConstructionException("Could not create command for BoatAction: " + action + ". Unknown BoatAction.");
}
}
}

@ -1,25 +1,24 @@
package mock.model.commandFactory;
import mock.model.MockRace;
import java.util.Stack;
import java.util.ArrayDeque;
import java.util.Queue;
/**
* Wraps multiple commands into a composite to execute queued commands during a frame.
*/
public class CompositeCommand implements Command {
private Stack<Command> commands;
private Queue<Command> commands;
public CompositeCommand() {
this.commands = new Stack<>();
this.commands = new ArrayDeque<>();
}
public void addCommand(Command command) {
commands.push(command);
commands.add(command);
}
@Override
public void execute(MockRace race) {
while(!commands.isEmpty()) commands.pop().execute(race);
public void execute() {
while(!commands.isEmpty()) commands.remove().execute();
}
}

@ -18,6 +18,9 @@ public class TackGybeCommand implements Command {
//The refactoring of MockRace will require changes to be made
@Override
public void execute() {
boat.setAutoVMG(false);
/*VMG newVMG = boat.getPolars().calculateVMG(
race.getWindDirection(),
race.getWindSpeed(),

@ -18,6 +18,7 @@ public class VMGCommand implements Command {
//The refactoring of MockRace will require changes to be made
@Override
public void execute() {
boat.setAutoVMG(true);
/*VMG newVMG = boat.getPolars().calculateVMG(
race.getWindDirection(),
race.getWindSpeed(),

@ -0,0 +1,37 @@
package mock.model.commandFactory;
import mock.model.MockBoat;
import mock.model.MockRace;
import shared.model.Bearing;
/**
* Created by connortaylorbrown on 4/08/17.
*/
public class WindCommand implements Command {
private MockRace race;
private MockBoat boat;
private int direction;
public WindCommand(MockRace race, MockBoat boat, boolean upwind) {
this.race = race;
this.boat = boat;
this.direction = upwind? -1 : 1;
}
@Override
public void execute() {
boat.setAutoVMG(false);
double wind = race.getWindDirection().degrees();
double heading = boat.getBearing().degrees();
double offset = 3.0;
offset *= direction;
double headWindDelta = wind - heading;
if ((headWindDelta < 0) || (headWindDelta > 180)) offset *= -1;
boat.setBearing(Bearing.fromDegrees(heading + offset));
}
}

@ -3,15 +3,12 @@ package network.StreamRelated;
import network.BinaryMessageDecoder;
import network.Exceptions.InvalidMessageException;
import network.MessageEncoders.RaceVisionByteEncoder;
import network.Messages.AC35Data;
import shared.model.RunnableWithFramePeriod;
import java.io.*;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -36,9 +33,11 @@ public class MessageDeserialiser implements RunnableWithFramePeriod {
/**
* Ack numbers used in messages.
* Determines whether or not this runnable is currently running.
*/
private int ackNumber = 1;
private boolean isRunning;
/**
@ -52,16 +51,6 @@ public class MessageDeserialiser implements RunnableWithFramePeriod {
}
/**
* Increments the ackNumber value, and returns it.
* @return Incremented ackNumber.
*/
private int getNextAckNumber(){
this.ackNumber++;
return this.ackNumber;
}
/**
@ -119,34 +108,35 @@ public class MessageDeserialiser implements RunnableWithFramePeriod {
}
/**
* 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;
//Reads the next message.
try {
AC35Data message = this.getNextMessage();
messagesRead.add(message);
}
catch (InvalidMessageException | IOException e) {
Logger.getGlobal().log(Level.WARNING, "Unable to read message.", e);
catch (InvalidMessageException e) {
Logger.getGlobal().log(Level.WARNING, "Unable to read message on thread: " + Thread.currentThread() + ".", e);
try {
inputStream.reset();
} catch (IOException e1) {
Logger.getGlobal().log(Level.WARNING, "Unable to reset inputStream.", e);
}
} catch (IOException e) {
Logger.getGlobal().log(Level.SEVERE, "Unable to read inputStream: " + inputStream + " on thread: " + Thread.currentThread() + ".", e);
isRunning = false;
return;
}

@ -81,7 +81,7 @@ public class MessageSerialiser implements RunnableWithFramePeriod {
isRunning = true;
while (isRunning) {
while (!Thread.interrupted()) {
long currentFrameTime = System.currentTimeMillis();
@ -104,8 +104,9 @@ public class MessageSerialiser implements RunnableWithFramePeriod {
Logger.getGlobal().log(Level.WARNING, "Could not encode message: " + message, e);
} catch (IOException e) {
Logger.getGlobal().log(Level.WARNING, "Could not write message to outputStream: " + outputStream, e);
Logger.getGlobal().log(Level.SEVERE, "Could not write message to outputStream: " + outputStream + " on thread: " + Thread.currentThread(), e);
isRunning = false;
return;
}
}

@ -19,6 +19,7 @@ import java.net.URL;
import java.net.UnknownHostException;
import java.util.ResourceBundle;
//TODO it appears this view/controller was replaced by Lobby.fxml. Remove?
/**
* Controls the connection that the VIsualiser can connect to.
*/
@ -82,11 +83,6 @@ public class ConnectionController extends Controller {
private ObservableList<RaceConnection> connections;
/**
* Represents whether the client is currently hosting a game already - this is to ensure they don't launch multiple servers.
*/
private boolean currentlyHostingGame = false;
@Override
public void initialize(URL location, ResourceBundle resources) {

@ -47,7 +47,7 @@ public class HostController extends Controller {
*/
public void hostGamePressed() throws IOException{
try {
Event game = new Event();
Event game = new Event(true);
game.start();
connectSocket("localhost", 4942);
} catch (EventConstructionException e) {
@ -78,7 +78,6 @@ public class HostController extends Controller {
*/
public void hostGame(){
hostWrapper.setVisible(true);
System.out.println("Reacted hostGame");
}
}

@ -58,7 +58,6 @@ public class LobbyController extends Controller {
}
else {
joinGameBtn.setDisable(true);
System.out.println(curr.statusProperty().getValue());
}
});
joinGameBtn.setDisable(true);
@ -70,11 +69,6 @@ public class LobbyController extends Controller {
public void refreshBtnPressed(){
for(RaceConnection connection: connections) {
connection.check();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
if (lobbyTable.getSelectionModel().getSelectedItem().statusProperty().getValue().equals("Ready")) {

@ -5,6 +5,7 @@ import javafx.fxml.FXML;
import javafx.scene.layout.AnchorPane;
import visualiser.app.VisualiserInput;
import visualiser.gameController.ControllerClient;
import visualiser.model.ServerConnection;
import visualiser.model.VisualiserBoat;
import visualiser.model.VisualiserRace;
@ -36,12 +37,12 @@ public class MainController extends Controller {
/**
* Transitions from the StartController screen (displays pre-race information) to the RaceController (displays the actual race).
* @param visualiserInput The object used to read packets from the race server.
* @param visualiserRace The object modelling the race.
* @param controllerClient Socket Client that manipulates the controller.
* @param serverConnection The connection to the server.
*/
public void beginRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace, ControllerClient controllerClient) {
raceController.startRace(visualiserInput, visualiserRace, controllerClient);
public void beginRace(VisualiserRace visualiserRace, ControllerClient controllerClient, ServerConnection serverConnection) {
raceController.startRace(visualiserRace, controllerClient, serverConnection);
}
/**
@ -103,10 +104,15 @@ public class MainController extends Controller {
AnchorPane.setLeftAnchor(startController.startWrapper(), 0.0);
AnchorPane.setRightAnchor(startController.startWrapper(), 0.0);
AnchorPane.setTopAnchor(connectionController.startWrapper(), 0.0);
AnchorPane.setBottomAnchor(connectionController.startWrapper(), 0.0);
AnchorPane.setLeftAnchor(connectionController.startWrapper(), 0.0);
AnchorPane.setRightAnchor(connectionController.startWrapper(), 0.0);
AnchorPane.setTopAnchor(lobbyController.startWrapper(), 0.0);
AnchorPane.setBottomAnchor(lobbyController.startWrapper(), 0.0);
AnchorPane.setLeftAnchor(lobbyController.startWrapper(), 0.0);
AnchorPane.setRightAnchor(lobbyController.startWrapper(), 0.0);
AnchorPane.setTopAnchor(hostController.startWrapper(), 0.0);
AnchorPane.setBottomAnchor(hostController.startWrapper(), 0.0);
AnchorPane.setLeftAnchor(hostController.startWrapper(), 0.0);
AnchorPane.setRightAnchor(hostController.startWrapper(), 0.0);
AnchorPane.setTopAnchor(finishController.finishWrapper, 0.0);
AnchorPane.setBottomAnchor(finishController.finishWrapper, 0.0);

@ -34,10 +34,6 @@ import java.util.logging.Logger;
*/
public class RaceController extends Controller {
/**
* The object used to read packets from the connected server.
*/
private VisualiserInput visualiserInput;
/**
* The race object which describes the currently occurring race.
@ -69,6 +65,11 @@ public class RaceController extends Controller {
*/
private ControllerClient controllerClient;
/**
* The connection to the server.
*/
private ServerConnection serverConnection;
@FXML private GridPane canvasBase;
@ -351,15 +352,15 @@ public class RaceController extends Controller {
/**
* Displays a specified race.
* @param visualiserInput Object used to read packets from server.
* @param visualiserRace Object modelling the race.
* @param controllerClient Socket Client that manipulates the controller.
* @param serverConnection The connection to the server.
*/
public void startRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace, ControllerClient controllerClient) {
public void startRace(VisualiserRace visualiserRace, ControllerClient controllerClient, ServerConnection serverConnection) {
this.visualiserInput = visualiserInput;
this.visualiserRace = visualiserRace;
this.controllerClient = controllerClient;
this.serverConnection = serverConnection;
initialiseRace();
@ -420,6 +421,15 @@ public class RaceController extends Controller {
}
//Return to main screen if we lose connection.
if (!serverConnection.isAlive()) {
race.setVisible(false);
parent.enterTitle();
//TODO currently this doesn't work correctly - the title screen remains visible after clicking join game
//TODO we should display an error to the user
//TODO also need to "reset" any state (race, connections, etc...).
}
}
}.start();
}

@ -74,6 +74,11 @@ public class StartController extends Controller implements Observer {
*/
private ServerConnection serverConnection;
/**
* Writes BoatActions to outgoing message queue.
*/
private ControllerClient controllerClient;
/**
* The race object which describes the currently occurring race.
@ -85,7 +90,7 @@ public class StartController extends Controller implements Observer {
/**
* An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor.
*/
List<Color> colors = new ArrayList<>(Arrays.asList(
private List<Color> colors = new ArrayList<>(Arrays.asList(
Color.BLUEVIOLET,
Color.BLACK,
Color.RED,
@ -130,6 +135,14 @@ public class StartController extends Controller implements Observer {
this.visualiserRace = new VisualiserRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, this.colors);
new Thread(this.visualiserRace).start();
//Ugly. TODO refactor
ObservableList<VisualiserBoat> boats = visualiserRace.getBoats();
for (VisualiserBoat boat : boats) {
if (boat.getSourceID() == serverConnection.getAllocatedSourceID()) {
boat.setClientBoat(true);
}
}
//Initialise the boat table.
initialiseBoatTable(this.visualiserRace);
@ -264,7 +277,7 @@ public class StartController extends Controller implements Observer {
startWrapper.setVisible(false);
start.setVisible(false);
parent.beginRace(visualiserInput, visualiserRace, controllerClient);
parent.beginRace(visualiserRace, controllerClient, serverConnection);
}
}
}.start();
@ -315,6 +328,7 @@ public class StartController extends Controller implements Observer {
LatestMessages latestMessages = new LatestMessages();
this.serverConnection = new ServerConnection(socket, latestMessages);
this.controllerClient = serverConnection.getControllerClient();
//Store a reference to latestMessages so that we can observe it.

@ -1,17 +1,11 @@
package visualiser.app;
import network.BinaryMessageDecoder;
import network.Exceptions.InvalidMessageException;
import network.Messages.*;
import shared.model.RunnableWithFramePeriod;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;
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;
/**
* TCP client which receives packets/messages from a race data source
@ -29,7 +23,6 @@ public class VisualiserInput implements RunnableWithFramePeriod {
private long lastHeartbeatSequenceNum = -1;
/**
* Incoming messages from server.
*/
@ -45,7 +38,8 @@ public class VisualiserInput implements RunnableWithFramePeriod {
/**
* Constructs a visualiserInput to convert an incoming stream of messages into LatestMessages.
* @param latestMessages Object to place messages in.
*
* @param latestMessages Object to place messages in.
* @param incomingMessages The incoming queue of messages.
*/
public VisualiserInput(LatestMessages latestMessages, BlockingQueue<AC35Data> incomingMessages) {
@ -58,6 +52,7 @@ public class VisualiserInput implements RunnableWithFramePeriod {
/**
* Returns the LatestMessages object, which can be queried for any received race related messages.
*
* @return The LatestMessages object.
*/
public LatestMessages getLatestMessages() {
@ -66,6 +61,7 @@ public class VisualiserInput implements RunnableWithFramePeriod {
/**
* Calculates the time since last heartbeat, in milliseconds.
*
* @return Time since last heartbeat, in milliseconds..
*/
private double timeSinceHeartbeat() {
@ -74,22 +70,176 @@ public class VisualiserInput implements RunnableWithFramePeriod {
}
@Override
public void run() {
//Handshake.
while (!Thread.interrupted()) {
//Main loop.
// take message
// create command
// place in command queue
AC35Data message = null;
try {
message = incomingMessages.take();
} catch (InterruptedException e) {
Logger.getGlobal().log(Level.SEVERE, "VisualiserInput was interrupted on thread: " + Thread.currentThread() + " while waiting for messages.");
Thread.currentThread().interrupt();
return;
}
//TODO refactor below
//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;
}
}
//Main loop.
// take message
// create command
// place in command queue
}
}
}

@ -1,18 +1,15 @@
package visualiser.gameController;
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.BinaryMessageDecoder;
import network.Exceptions.InvalidMessageException;
import network.MessageDecoders.BoatActionDecoder;
import network.Messages.AC35Data;
import network.Messages.BoatAction;
import network.Messages.Enums.MessageType;
import shared.model.RunnableWithFramePeriod;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -20,7 +17,7 @@ import java.util.logging.Logger;
/**
* Service for dispatching key press data to race from client
*/
public class ControllerServer implements Runnable {
public class ControllerServer implements RunnableWithFramePeriod {
/**
@ -34,6 +31,11 @@ public class ControllerServer implements Runnable {
*/
private CompositeCommand compositeCommand;
/**
* The context for each command.
*/
private MockRace raceState;
/**
* This is the source ID associated with the client.
*/
@ -46,15 +48,18 @@ public class ControllerServer implements Runnable {
* @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) {
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.
*/
@ -70,10 +75,17 @@ public class ControllerServer implements Runnable {
BoatAction boatAction = (BoatAction) message;
boatAction.setSourceID(clientSourceID);
Command command = CommandFactory.createCommand(boatAction);
compositeCommand.addCommand(command);
try {
Command command = CommandFactory.createCommand(raceState, boatAction);
compositeCommand.addCommand(command);
} catch (CommandConstructionException e) {
Logger.getGlobal().log(Level.WARNING, "ControllerServer could not create a Command for BoatAction: " + boatAction + ".", e);
}
}
@ -81,6 +93,7 @@ public class ControllerServer implements Runnable {
Logger.getGlobal().log(Level.WARNING, "ControllerServer Interrupted while waiting for message on incoming message queue.", e);
Thread.currentThread().interrupt();
return;
}
}

@ -22,16 +22,14 @@ public class Ping implements Runnable {
this.rc = rc;
}
public boolean pingPort(){
//TODO the connection needs to be moved to its own thread, so it doesn't block fx thread.
public boolean pingPort() {
InetSocketAddress i = new InetSocketAddress(hostname, port);
try (Socket s = new Socket()){
s.connect(i, 750);//TODO this should be at least a second or two, once moved to its own thread
s.connect(i, 1500);
s.shutdownInput();
s.shutdownOutput();
s.close();
rc.statusProperty().set("Ready");
//System.out.println(String.valueOf(s.isClosed()));
return true;
} catch (IOException e) {
rc.statusProperty().set("Offline");

@ -314,6 +314,10 @@ public class ResizableRaceCanvas extends ResizableCanvas {
//The position may be null if we haven't received any BoatLocation messages yet.
if (boat.getCurrentPosition() != null) {
if (boat.isClientBoat()) {
drawClientBoat(boat);
}
//Convert position to graph coordinate.
GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
@ -343,6 +347,39 @@ public class ResizableRaceCanvas extends ResizableCanvas {
}
/**
* Draws extra decorations to show which boat has been assigned to the client.
* @param boat The client's boat.
*/
private void drawClientBoat(VisualiserBoat boat) {
//Convert position to graph coordinate.
GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
//The x coordinates of each vertex of the boat.
double[] x = {
pos.getX() - 12,
pos.getX(),
pos.getX() + 12 };
//The y coordinates of each vertex of the boat.
double[] y = {
pos.getY() + 24,
pos.getY() - 24,
pos.getY() + 24 };
//The above shape is essentially a triangle 24px wide, and 48 long.
//Draw the boat.
gc.setFill(Color.BLACK);
gc.save();
rotate(boat.getBearing().degrees(), pos.getX(), pos.getY());
gc.fillPolygon(x, y, 3);
gc.restore();
}
/**
* Draws the wake for a given boat.

@ -1,12 +1,7 @@
package visualiser.model;
import mock.app.MockOutput;
import mock.enums.ConnectionStateEnum;
import mock.exceptions.SourceIDAllocationException;
import mock.model.HeartBeatService;
import mock.model.SourceIdAllocator;
import mock.model.commandFactory.CompositeCommand;
import network.Messages.AC35Data;
import network.Messages.Enums.JoinAcceptanceEnum;
import network.Messages.Enums.MessageType;
@ -19,7 +14,6 @@ import network.StreamRelated.MessageSerialiser;
import shared.exceptions.HandshakeException;
import visualiser.app.VisualiserInput;
import visualiser.gameController.ControllerClient;
import visualiser.gameController.ControllerServer;
import java.io.IOException;
import java.net.Socket;
@ -38,6 +32,11 @@ public class ServerConnection implements Runnable {
*/
private Socket socket;
/**
* The source ID that has been allocated to the client.
*/
private int allocatedSourceID = 0;
/**
* Latest snapshot of the race, received from the server.
@ -106,6 +105,9 @@ public class ServerConnection implements Runnable {
new Thread(messageSerialiser, "ServerConnection()->MessageSerialiser thread " + messageSerialiser).start();
new Thread(messageDeserialiser, "ServerConnection()->MessageDeserialiser thread " + messageDeserialiser).start();
this.controllerClient = new ControllerClient(outputQueue);
}
@ -135,25 +137,23 @@ public class ServerConnection implements Runnable {
connectionState = ConnectionStateEnum.WAITING_FOR_HANDSHAKE;
sendJoinAcceptanceMessage(RequestToJoinEnum.PARTICIPANT);
sendRequestToJoinMessage(RequestToJoinEnum.PARTICIPANT);
JoinAcceptance joinAcceptance = waitForJoinAcceptance();
int allocatedSourceID = 0;
//If we join successfully...
if (joinAcceptance.getAcceptanceType() == JoinAcceptanceEnum.JOIN_SUCCESSFUL) {
allocatedSourceID = joinAcceptance.getSourceID();
//TODO need to do something with the ID - maybe flag the correct visualiser boat as being the client's boat?
this.allocatedSourceID = joinAcceptance.getSourceID();
this.controllerClient = new ControllerClient(inputQueue);
//new Thread(controllerClient, "ServerConnection.run()->ControllerClient thread " + controllerClient).start();
}
this.visualiserInput = new VisualiserInput(latestMessages, outputQueue);
this.visualiserInput = new VisualiserInput(latestMessages, inputQueue);
new Thread(visualiserInput, "ServerConnection.run()->VisualiserInput thread " + visualiserInput).start();
@ -200,7 +200,7 @@ public class ServerConnection implements Runnable {
* @param requestType The type of request to send
* @throws HandshakeException Thrown if the thread is interrupted while placing message on the outgoing message queue.
*/
private void sendJoinAcceptanceMessage(RequestToJoinEnum requestType) throws HandshakeException {
private void sendRequestToJoinMessage(RequestToJoinEnum requestType) throws HandshakeException {
//Send them the source ID.
RequestToJoin requestToJoin = new RequestToJoin(requestType);
@ -217,12 +217,27 @@ public class ServerConnection implements Runnable {
/**
* Determines whether or not this connection is still alive.
* This is based off whether the {@link MessageSerialiser} is still alive.
* This is based off whether the {@link MessageDeserialiser} is still alive.
* @return True if it is alive, false otherwise.
*/
public boolean isAlive() {
return messageSerialiser.isRunning();
return messageDeserialiser.isRunning();
}
/**
* Returns the controller client, which writes BoatAction messages to the outgoing queue.
* @return The ControllerClient.
*/
public ControllerClient getControllerClient() {
return controllerClient;
}
/**
* Returns the source ID that has been allocated to the client.
* @return Source ID allocated to the client. 0 if it hasn't been allocated.
*/
public int getAllocatedSourceID() {
return allocatedSourceID;
}
}

@ -57,6 +57,11 @@ public class VisualiserBoat extends Boat {
*/
private static final double wakeScale = 5;
/**
* If true then this boat has been allocated to the client.
*/
private boolean isClientBoat = false;
@ -214,4 +219,21 @@ public class VisualiserBoat extends Boat {
return " -";
}
}
/**
* Returns whether or not this boat has been assigned to the client.
* @return True if this is the client's boat.
*/
public boolean isClientBoat() {
return isClientBoat;
}
/**
* Sets whether or not this boat has been assigned to the client.
* @param clientBoat True if this is the client's boat.
*/
public void setClientBoat(boolean clientBoat) {
isClientBoat = clientBoat;
}
}

@ -347,13 +347,16 @@ public class VisualiserRace extends Race implements Runnable {
@Override
public void handle(long arg0) {
//Calculate the frame period.
long currentFrameTime = System.currentTimeMillis();
long framePeriod = currentFrameTime - lastFrameTime;
//Update race status.
updateRaceStatus(latestMessages.getRaceStatus());
if (latestMessages.getRaceStatus() != null) {
updateRaceStatus(latestMessages.getRaceStatus());
}
//Update racing boats.

@ -33,8 +33,24 @@
</Boat>
<!--Participants-->
<!--Participants-->
<Boat BoatName="Emirates Team New Zealand" HullNum="RG01" ShapeID="0" ShortName="NZL" SourceID="121" StoweName="NZL" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Land Rover BAR" HullNum="RG01" ShapeID="0" ShortName="GBR" SourceID="122" StoweName="GBR" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="SoftBank Team Japan" HullNum="RG01" ShapeID="0" ShortName="JPN" SourceID="123" StoweName="JPN" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Groupama Team France" HullNum="RG01" ShapeID="0" ShortName="FRA" SourceID="124" StoweName="FRA" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Artemis Racing" HullNum="RG01" ShapeID="0" ShortName="SWE" SourceID="125" StoweName="SWE" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Emirates Team New Zealand" HullNum="RG01" ShapeID="0" ShortName="NZL" SourceID="126" StoweName="NZL" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
</Boats>
</BoatConfig>

@ -36,20 +36,5 @@
<Boat BoatName="Emirates Team New Zealand" HullNum="RG01" ShapeID="0" ShortName="NZL" SourceID="126" StoweName="NZL" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Land Rover BAR" HullNum="RG01" ShapeID="0" ShortName="GBR" SourceID="122" StoweName="GBR" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="SoftBank Team Japan" HullNum="RG01" ShapeID="0" ShortName="JPN" SourceID="123" StoweName="JPN" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Groupama Team France" HullNum="RG01" ShapeID="0" ShortName="FRA" SourceID="124" StoweName="FRA" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Artemis Racing" HullNum="RG01" ShapeID="0" ShortName="SWE" SourceID="125" StoweName="SWE" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Emirates Team New Zealand" HullNum="RG01" ShapeID="0" ShortName="NZL" SourceID="126" StoweName="NZL" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
</Boats>
</BoatConfig>

@ -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 CompoundMarkID="1" SeqID="1"/>
<Corner CompoundMarkID="2" SeqID="2"/>
<Corner CompoundMarkID="4" SeqID="3"/>
<Corner CompoundMarkID="3" SeqID="4"/>
<Corner CompoundMarkID="4" SeqID="5"/>
<Corner CompoundMarkID="5" SeqID="6"/>
</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>

@ -5,7 +5,12 @@
<CreationTimeDate>CREATION_TIME</CreationTimeDate>
<RaceStartTime Postpone="false" Time="START_TIME"/>
<Participants>
<Yacht SourceID="121"/>
<Yacht SourceID="122"/>
<Yacht SourceID="123"/>
<Yacht SourceID="124"/>
<Yacht SourceID="125"/>
<Yacht SourceID="126"/>
</Participants>
<CompoundMarkSequence>
<Corner CompoundMarkID="1" SeqID="1"/>

@ -1,13 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import javafx.scene.text.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<AnchorPane fx:id="connectionWrapper" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0" prefWidth="780.0" visible="false" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.ConnectionController">
<AnchorPane fx:id="connectionWrapper" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0" prefWidth="780.0" visible="false" xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.ConnectionController">
<children>
<GridPane fx:id="connection" prefHeight="600.0" prefWidth="780.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<columnConstraints>

@ -1,13 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import javafx.scene.text.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.text.Font?>
<AnchorPane fx:id="lobbyWrapper" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0" prefWidth="780.0" visible="false" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.LobbyController">
<AnchorPane fx:id="lobbyWrapper" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0" prefWidth="780.0" visible="false" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.LobbyController">
<children>
<GridPane fx:id="connection" prefHeight="600.0" prefWidth="780.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<columnConstraints>
@ -35,31 +40,52 @@
<Font size="36.0" />
</font>
</Label>
<Button fx:id="joinGameBtn" mnemonicParsing="false" onAction="#connectSocket" text="Connect to Game" GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="2">
<GridPane fx:id="buttonsGridPane" GridPane.columnIndex="1" GridPane.rowIndex="2">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Button fx:id="joinGameBtn" mnemonicParsing="false" onAction="#connectSocket" text="Connect to Game" GridPane.columnIndex="2">
<GridPane.margin>
<Insets right="20.0" />
</GridPane.margin></Button>
<Button mnemonicParsing="false" onAction="#refreshBtnPressed" text="Refresh" GridPane.columnIndex="1">
<GridPane.margin>
<Insets left="10.0" right="10.0" />
</GridPane.margin></Button>
<Button mnemonicParsing="false" onAction="#addConnectionPressed" text="Add">
<GridPane.margin>
<Insets left="20.0" />
</GridPane.margin></Button>
</children>
</GridPane>
<GridPane fx:id="ipPortGridPane" GridPane.rowIndex="2">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<TextField fx:id="addressFld" promptText="Address">
<GridPane.margin>
<Insets left="20.0" right="20.0" />
</GridPane.margin></TextField>
<TextField fx:id="portFld" promptText="Port Number" GridPane.columnIndex="1">
<GridPane.margin>
<Insets left="20.0" right="20.0" />
</GridPane.margin></TextField>
</children>
<GridPane.margin>
<Insets right="50.0" />
<Insets />
</GridPane.margin>
</Button>
<Button mnemonicParsing="false" onAction="#refreshBtnPressed" text="Refresh" GridPane.columnIndex="1" GridPane.halignment="LEFT" GridPane.rowIndex="2">
<GridPane.margin>
<Insets left="120.0" />
</GridPane.margin>
</Button>
<TextField fx:id="addressFld" promptText="Address" GridPane.rowIndex="2">
<GridPane.margin>
<Insets left="50.0" right="150.0" />
</GridPane.margin>
</TextField>
<TextField fx:id="portFld" promptText="Port Number" GridPane.rowIndex="2">
<GridPane.margin>
<Insets left="270.0" />
</GridPane.margin>
</TextField>
<Button mnemonicParsing="false" onAction="#addConnectionPressed" text="Add" GridPane.columnIndex="1" GridPane.rowIndex="2">
<GridPane.margin>
<Insets left="40.0" />
</GridPane.margin>
</Button>
</GridPane>
</children>
</GridPane>
</children>

@ -14,6 +14,13 @@ import static org.junit.Assert.fail;
public class PolarParserTest {
public static Polars createPolars() {
Polars polars = PolarParser.parse("mock/polars/acc_polars.csv");
return polars;
}
/**
* Tests if we can parse a valid polar data file (stored in a string), and create a polar table.
*/

@ -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,44 @@
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
* @throws InvalidRaceDataException
* @throws InvalidRegattaDataException
*/
public static MockRace createMockRace() throws InvalidBoatDataException, InvalidRaceDataException, InvalidRegattaDataException {
BoatDataSource boatDataSource = BoatXMLReaderTest.createBoatDataSource();
RaceDataSource raceDataSource = RaceXMLReaderTest.createRaceDataSource();
RegattaDataSource regattaDataSource = RegattaXMLReaderTest.createRegattaDataSource();
LatestMessages latestMessages = new LatestMessages();
Polars polars = PolarParserTest.createPolars();
WindGenerator windGenerator = new ConstantWindGenerator(Bearing.fromDegrees(230), 10);
MockRace mockRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, polars, Constants.RaceTimeScale, windGenerator);
return mockRace;
}
}

@ -8,10 +8,10 @@ import shared.model.Wind;
import static org.junit.Assert.*;
public class WindGeneratorTest {
public class RandomWindGeneratorTest {
private WindGenerator windGenerator;
private RandomWindGenerator randomWindGenerator;
private Bearing windBaselineBearing;
private Bearing windBearingLowerBound;
@ -36,7 +36,7 @@ public class WindGeneratorTest {
this.windSpeedLowerBound = 7;
this.windSpeedUpperBound = 20;
this.windGenerator = new WindGenerator(
this.randomWindGenerator = new RandomWindGenerator(
windBaselineBearing,
windBearingLowerBound,
windBearingUpperBound,
@ -55,7 +55,7 @@ public class WindGeneratorTest {
@Test
public void generateBaselineWindTest() {
Wind wind = windGenerator.generateBaselineWind();
Wind wind = randomWindGenerator.generateBaselineWind();
assertEquals(windBaselineSpeed, wind.getWindSpeed(), speedKnotsEpsilon);
assertEquals(windBaselineBearing.degrees(), wind.getWindDirection().degrees(), bearingDegreeEpsilon);
@ -72,7 +72,7 @@ public class WindGeneratorTest {
for (int i = 0; i < randomWindCount; i++) {
Wind wind = windGenerator.generateRandomWind();
Wind wind = randomWindGenerator.generateRandomWind();
assertTrue(wind.getWindSpeed() >= windSpeedLowerBound);
assertTrue(wind.getWindSpeed() <= windSpeedUpperBound);
@ -91,13 +91,13 @@ public class WindGeneratorTest {
@Test
public void generateNextWindTest() {
Wind wind = windGenerator.generateBaselineWind();
Wind wind = randomWindGenerator.generateBaselineWind();
int randomWindCount = 1000;
for (int i = 0; i < randomWindCount; i++) {
wind = windGenerator.generateNextWind(wind);
wind = randomWindGenerator.generateNextWind(wind);
assertTrue(wind.getWindSpeed() >= windSpeedLowerBound);
assertTrue(wind.getWindSpeed() <= windSpeedUpperBound);

@ -1,31 +1,72 @@
package mock.model.commandFactory;
import mock.exceptions.CommandConstructionException;
import mock.model.MockBoat;
import mock.model.MockRace;
import mock.model.MockRaceTest;
import network.Messages.BoatAction;
import network.Messages.Enums.BoatActionEnum;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import shared.exceptions.InvalidBoatDataException;
import shared.exceptions.InvalidRaceDataException;
import shared.exceptions.InvalidRegattaDataException;
import shared.model.Bearing;
import shared.model.Boat;
import shared.model.Race;
import visualiser.model.VisualiserRace;
import static org.mockito.Mockito.when;
import static org.testng.Assert.*;
import static org.mockito.Mockito.mock;
/**
* Created by connortaylorbrown on 4/08/17.
*/
public class WindCommandTest {
private Race race;
private Boat boat;
private MockRace race;
private MockBoat boat;
private Command upwind;
private Command downwind;
private double initial;
private double offset = 3.0;
@Before
public void setUp() {
boat = new Boat(0, "Bob", "NZ");
public void setUp() throws CommandConstructionException, InvalidBoatDataException, InvalidRegattaDataException, InvalidRaceDataException {
race = MockRaceTest.createMockRace();
boat = race.getBoats().get(0);
//when(race.getWindDirection()).thenReturn(Bearing.fromDegrees(0.0));
boat.setBearing(Bearing.fromDegrees(45.0));
BoatAction upwindAction = new BoatAction(BoatActionEnum.UPWIND);
upwindAction.setSourceID(boat.getSourceID());
BoatAction downwindAction = new BoatAction(BoatActionEnum.DOWNWIND);
downwindAction.setSourceID(boat.getSourceID());
upwind = CommandFactory.createCommand(race, upwindAction);
downwind = CommandFactory.createCommand(race, downwindAction);
initial = boat.getBearing().degrees();
}
/**
* Ensure the difference between initial and final angle is 3 degrees
*/
@Test
public void upwindCommandDecreasesAngle() {
upwind.execute();
assertEquals(initial - boat.getBearing().degrees(), -offset, 1e-5);
}
@Test
public void downwindCommandIncreasesAngle() {
downwind.execute();
assertEquals(initial - boat.getBearing().degrees(), offset, 1e-5);
}
}

@ -31,31 +31,40 @@ public class BoatXMLReaderTest {
private List<Mark> marks;
@Before
public void setUp() {
public static BoatDataSource createBoatDataSource() throws InvalidBoatDataException {
String boatXMLString = null;
try {
boatXMLString = XMLReader.readXMLFileToString("mock/mockXML/boatTest.xml", StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
fail("could not read boat xml file into a string");
} catch (TransformerException | XMLReaderException e) {
throw new InvalidBoatDataException("Could not read boat XML file into a string.", e);
}
try {
boatData = new BoatXMLReader(boatXMLString, XMLFileType.Contents);
BoatDataSource boatData = new BoatXMLReader(boatXMLString, XMLFileType.Contents);
return boatData;
boats = new ArrayList<>(boatData.getBoats().values());
marks = new ArrayList<>(boatData.getMarkerBoats().values());
} catch (XMLReaderException | InvalidBoatDataException e) {
e.printStackTrace();
assertTrue(false);
} catch (XMLReaderException e) {
throw new InvalidBoatDataException("Could not parse boat XML file.", e);
}
}
@Before
public void setUp() throws InvalidBoatDataException {
boatData = createBoatDataSource();
boats = new ArrayList<>(boatData.getBoats().values());
marks = new ArrayList<>(boatData.getMarkerBoats().values());
}
@Test

@ -1,5 +1,39 @@
package shared.dataInput;
import shared.enums.XMLFileType;
import shared.exceptions.InvalidRaceDataException;
import shared.exceptions.XMLReaderException;
import javax.xml.transform.TransformerException;
import java.nio.charset.StandardCharsets;
public class RaceXMLReaderTest {
//TODO
public static RaceDataSource createRaceDataSource() throws InvalidRaceDataException {
String raceXMLString = null;
try {
raceXMLString = XMLReader.readXMLFileToString("mock/mockXML/raceTest.xml", StandardCharsets.UTF_8);
} catch (TransformerException | XMLReaderException e) {
throw new InvalidRaceDataException("Could not read race XML file into a string.", e);
}
try {
RaceXMLReader raceData = new RaceXMLReader(raceXMLString, XMLFileType.Contents);
return raceData;
} catch (XMLReaderException e) {
throw new InvalidRaceDataException("Could not parse race XML file.", e);
}
}
}

@ -3,6 +3,11 @@ package shared.dataInput;
import org.junit.Before;
import org.junit.Test;
import shared.enums.XMLFileType;
import shared.exceptions.InvalidRegattaDataException;
import shared.exceptions.XMLReaderException;
import javax.xml.transform.TransformerException;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@ -14,6 +19,34 @@ import static org.junit.Assert.fail;
public class RegattaXMLReaderTest {
private RegattaXMLReader regatta;
public static RegattaDataSource createRegattaDataSource() throws InvalidRegattaDataException {
String regattaXMLString = null;
try {
regattaXMLString = XMLReader.readXMLFileToString("mock/mockXML/regattaTest.xml", StandardCharsets.UTF_8);
} catch (TransformerException | XMLReaderException e) {
throw new InvalidRegattaDataException("Could not read regatta XML file into a string.", e);
}
try {
RegattaDataSource raceData = new RegattaXMLReader(regattaXMLString, XMLFileType.Contents);
return raceData;
} catch (XMLReaderException e) {
throw new InvalidRegattaDataException("Could not parse regatta XML file.", e);
}
}
@Before
public void findFile() {
try {

Loading…
Cancel
Save