Added empty data sources, to be used by VisualiserRace.

Created FrameRateTracker which can be used to track framerate.
Updated RequestToJoinEnum and JoinAcceptanceEnum to match the agreed connection API.
Added AssignPlayerBoat message, which is used internally on the client to assign the player a source ID once they have connected.
Fixed some race conditions in the MessageRouter.
Updated ConnectionAcceptor.CheckClientConnection to wait slightly longer before removing connection (there was a slight race condition before).
Race no longer has a reference to LatestMessages. LatestMessages no longer has specific messages types in it.
Created RaceState class, which contains the state that is shared between VisualiserRaceState and MockRaceState (currently only used on visualiser).
Split VisualiserRace into VisualiserRaceState and VisualiserRaceService.
Added the VisualiserRace commands (BoatLocatonCommand, RaceStatusCommand, etc...).
Slightly increased the preferred width of race.fxml table columns.
issues #27 #37 #35
#story[1095]
main
fjc40 8 years ago
parent 89b0aa8b77
commit 7366aba5ec

@ -13,7 +13,12 @@ import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -36,7 +41,7 @@ public class ConnectionAcceptor implements Runnable {
/**
* List of client connections.
*/
private ArrayBlockingQueue<ClientConnection> clientConnections = new ArrayBlockingQueue<>(16, true);
private BlockingQueue<ClientConnection> clientConnections = new ArrayBlockingQueue<>(16, true);
/**
* Snapshot of the race.
@ -137,13 +142,13 @@ public class ConnectionAcceptor implements Runnable {
*/
class CheckClientConnection implements Runnable{
private ArrayBlockingQueue<ClientConnection> connections;
private BlockingQueue<ClientConnection> connections;
/**
* Constructor
* @param connections Clients "connected"
*/
public CheckClientConnection(ArrayBlockingQueue<ClientConnection> connections){
public CheckClientConnection(BlockingQueue<ClientConnection> connections){
this.connections = connections;
}
@ -153,13 +158,29 @@ public class ConnectionAcceptor implements Runnable {
@Override
public void run() {
while(true) {
//System.out.println(connections.size());//used to see current amount of visualisers connected.
ArrayBlockingQueue<ClientConnection> clientConnections = new ArrayBlockingQueue<>(16, true, connections);
//We track the number of times each connection fails the !isAlive() test.
//This is to give a bit of lee-way in case the connection checker checks a connection before its thread has actually started.
Map<ClientConnection, Integer> connectionDeadCount = new HashMap<>();
while(!Thread.interrupted()) {
//Make copy of connections.
List<ClientConnection> clientConnections = new ArrayList<>(connections);
for (ClientConnection client : clientConnections) {
connectionDeadCount.put(client, connectionDeadCount.getOrDefault(client, 0));
if (!client.isAlive()) {
//Add one to fail count.
connectionDeadCount.put(client, connectionDeadCount.get(client) + 1);
}
//We only remove them if they fail 5 times.
if (connectionDeadCount.get(client) > 5) {
connections.remove(client);
connectionDeadCount.remove(client);
client.terminate();
Logger.getGlobal().log(Level.WARNING, "CheckClientConnection is removing the dead connection: " + client);

@ -127,7 +127,6 @@ public class Event {
boatDataSource,
raceDataSource,
regattaDataSource,
this.latestMessages,
this.boatPolars,
Constants.RaceTimeScale,
windGenerator ),

@ -245,7 +245,7 @@ public class ClientConnection implements Runnable {
private void sendJoinAcceptanceMessage(int sourceID) throws HandshakeException {
//Send them the source ID.
JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID);
JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, sourceID);
try {
outputQueue.put(joinAcceptance);

@ -53,14 +53,13 @@ public class MockRace extends Race {
* @param boatDataSource Data source for boat related data (yachts and marker boats).
* @param raceDataSource Data source for race related data (participating boats, legs, etc...).
* @param regattaDataSource Data source for race related data (course name, location, timezone, etc...).
* @param latestMessages The LatestMessages to send events to.
* @param polars The polars table to be used for boat simulation.
* @param timeScale The timeScale for the race. See {@link Constants#RaceTimeScale}.
* @param windGenerator The wind generator used for the race.
*/
public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, Polars polars, int timeScale, WindGenerator windGenerator) {
public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, Polars polars, int timeScale, WindGenerator windGenerator) {
super(boatDataSource, raceDataSource, regattaDataSource, latestMessages);
super(boatDataSource, raceDataSource, regattaDataSource);
this.scaleFactor = timeScale;
@ -456,7 +455,5 @@ public class MockRace extends Race {
}
public List<CompoundMark> getCompoundMarks() {
return compoundMarks;
}
}

@ -103,15 +103,19 @@ public class MessageRouter implements RunnableWithFramePeriod {
AC35Data message = incomingMessages.take();
if (routeMap.containsKey(message.getType())) {
//We have a route.
routeMap.get(message.getType()).put(message);
BlockingQueue<AC35Data> queue = routeMap.get(message.getType());
if (queue != null) {
queue.put(message);
} else {
//No route. Use default.
if (defaultRoute.isPresent()) {
defaultRoute.get().put(message);
BlockingQueue<AC35Data> defaultQueue = defaultRoute.orElse(null);
if (defaultQueue != null) {
defaultQueue.put(message);
}
}

@ -0,0 +1,39 @@
package network.Messages;
import network.Messages.Enums.JoinAcceptanceEnum;
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;
}
}

@ -128,7 +128,7 @@ public class BoatStatus {
}
/**
* Returns he time at which it is estimated the boat will reach the next mark. Milliseconds since unix epoch.
* Returns the time at which it is estimated the boat will reach the next mark. Milliseconds since unix epoch.
* @return Time at which boat will reach next mark.
*/
public long getEstTimeAtNextMark() {
@ -136,7 +136,7 @@ public class BoatStatus {
}
/**
* Returns he time at which it is estimated the boat will finish the race. Milliseconds since unix epoch.
* Returns the time at which it is estimated the boat will finish the race. Milliseconds since unix epoch.
* @return Time at which boat will finish the race.
*/
public long getEstTimeAtFinish() {

@ -11,24 +11,35 @@ public enum JoinAcceptanceEnum {
/**
* Client is allowed to join.
* Client is allowed to join and spectate.
*/
JOIN_SUCCESSFUL(1),
JOIN_SUCCESSFUL_SPECTATOR(0),
/**
* The race is full - no more participants allowed.
* Client is allowed to join and participate.
*/
RACE_PARTICIPANTS_FULL(2),
JOIN_SUCCESSFUL_PARTICIPANT(1),
/**
* The race cannot allow any more ghost participants to join.
* Client is allowed to join and play the tutorial.
*/
GHOST_PARTICIPANTS_FULL(3),
JOIN_SUCCESSFUL_TUTORIAL(2),
/**
* Client is allowed to join and participate as a ghost player.
*/
JOIN_SUCCESSFUL_GHOST(3),
/**
* Join Request was denied.
*/
JOIN_FAILURE(0x10),
/**
* The server is completely full, cannot participate or spectate.
*/
SERVER_FULL(4),
SERVER_FULL(0x11),
/**

@ -20,17 +20,25 @@ public enum MessageType {
COURSEWIND(44),
AVGWIND(47),
BOATACTION(100),
/**
* This is used for {@link network.Messages.RequestToJoin} messages.
*/
REQUEST_TO_JOIN(55),
REQUEST_TO_JOIN(101),
/**
* This is used for {@link network.Messages.JoinAcceptance} messages.
*/
JOIN_ACCEPTANCE(56),
JOIN_ACCEPTANCE(102),
/**
* This is used for {@link network.Messages.AssignPlayerBoat} messages.
*/
ASSIGN_PLAYER_BOAT(121),
BOATACTION(100),
NOTAMESSAGE(0);

@ -20,10 +20,15 @@ public enum RequestToJoinEnum {
*/
PARTICIPANT(1),
/**
* Client wants to play the tutorial.
*/
CONTROL_TUTORIAL(2),
/**
* Client wants to particpate as a ghost.
*/
GHOST(5),
GHOST(3),
/**

@ -1,7 +1,6 @@
package network.Messages;
import network.Messages.Enums.XMLMessageType;
import shared.dataInput.RaceDataSource;
import java.util.*;
@ -11,36 +10,6 @@ import java.util.*;
*/
public class LatestMessages extends Observable {
/**
* The latest RaceStatus message.
*/
private RaceStatus raceStatus;
/**
* A map of the last BoatStatus message received, for each boat.
*/
private final Map<Integer, BoatStatus> boatStatusMap = new HashMap<>();
/**
* A map of the last BoatLocation message received, for each boat.
*/
private final Map<Integer, BoatLocation> boatLocationMap = new HashMap<>();
/**
* A map of the last MarkRounding message received, for each boat.
*/
private final Map<Integer, MarkRounding> markRoundingMap = new HashMap<>();
/**
* The last AverageWind message received.
*/
private AverageWind averageWind;
/**
* The last CourseWinds message received.
*/
private CourseWinds courseWinds;
/**
* A list of messages containing a snapshot of the race.
@ -91,138 +60,7 @@ public class LatestMessages extends Observable {
}
/**
* Gets the latest RaceStatus message received.
* @return The latest RaceStatus message received.
*/
public RaceStatus getRaceStatus() {
return raceStatus;
}
/**
* Sets the latest RaceStatus message received.
* @param raceStatus The new RaceStatus message to store.
*/
public void setRaceStatus(RaceStatus raceStatus) {
this.raceStatus = raceStatus;
}
/**
* Returns the latest BoatStatus message received for a given boat.
* @param sourceID Source ID of the boat.
* @return The latest BoatStatus message for the specified boat.
*/
public BoatStatus getBoatStatus(int sourceID) {
return boatStatusMap.get(sourceID);
}
/**
* Inserts a BoatStatus message for a given boat.
* @param boatStatus The BoatStatus message to set.
*/
public void setBoatStatus(BoatStatus boatStatus) {
boatStatusMap.put(boatStatus.getSourceID(), boatStatus);
}
/**
* Returns the latest BoatLocation message received for a given boat.
* @param sourceID Source ID of the boat.
* @return The latest BoatLocation message for the specified boat.
*/
public BoatLocation getBoatLocation(int sourceID) {
return boatLocationMap.get(sourceID);
}
/**
* Inserts a BoatLocation message for a given boat.
* @param boatLocation The BoatLocation message to set.
*/
public void setBoatLocation(BoatLocation boatLocation) {
//TODO should compare the sequence number of the new boatLocation with the existing boatLocation for this boat (if it exists), and use the newer one.
boatLocationMap.put(boatLocation.getSourceID(), boatLocation);
}
/**
* Returns the latest MarkRounding message received for a given boat.
* @param sourceID Source ID of the boat.
* @return The latest MarkRounding message for the specified boat.
*/
public MarkRounding getMarkRounding(int sourceID) {
return markRoundingMap.get(sourceID);
}
/**
* Inserts a MarkRounding message for a given boat.
* @param markRounding The MarkRounding message to set.
*/
public void setMarkRounding(MarkRounding markRounding) {
//TODO should compare the sequence number of the new markRounding with the existing boatLocation for this boat (if it exists), and use the newer one.
markRoundingMap.put(markRounding.getSourceID(), markRounding);
}
/**
* Gets the latest AverageWind message received.
* @return The latest AverageWind message received.
*/
public AverageWind getAverageWind() {
return averageWind;
}
/**
* Sets the latest AverageWind message received.
* @param averageWind The new AverageWind message to store.
*/
public void setAverageWind(AverageWind averageWind) {
this.averageWind = averageWind;
}
/**
* Gets the latest CourseWinds message received.
* @return The latest CourseWinds message received.
*/
public CourseWinds getCourseWinds() {
return courseWinds;
}
/**
* Sets the latest CourseWinds message received.
* @param courseWinds The new CourseWinds message to store.
*/
public void setCourseWinds(CourseWinds courseWinds) {
this.courseWinds = courseWinds;
}
/**
* Returns the map of boat sourceIDs to BoatLocation messages.
* @return Map between boat sourceID and BoatLocation.
*/
public Map<Integer, BoatLocation> getBoatLocationMap() {
return boatLocationMap;
}
/**
* Returns the map of boat sourceIDs to BoatStatus messages.
* @return Map between boat sourceID and BoatStatus.
*/
public Map<Integer, BoatStatus> getBoatStatusMap() {
return boatStatusMap;
}
/**
* Returns the map of boat sourceIDs to MarkRounding messages.
* @return Map between boat sourceID and MarkRounding.
*/
public Map<Integer, MarkRounding> getMarkRoundingMap() {
return markRoundingMap;
}

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

@ -6,17 +6,17 @@ import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.Enums.RaceTypeEnum;
import network.Messages.LatestMessages;
import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import shared.dataInput.RegattaDataSource;
import visualiser.model.VisualiserRaceEvent;
import java.util.List;
/**
* Represents a yacht race.
* This is a base class inherited by {@link mock.model.MockRace} and {@link visualiser.model.VisualiserRace}.
* This is a base class inherited by {@link mock.model.MockRace} and {@link VisualiserRaceEvent}.
* 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 Race {
@ -37,11 +37,6 @@ public abstract class Race {
*/
protected RegattaDataSource regattaDataSource;
/**
* The collection of latest race messages.
* Can be either read from or written to.
*/
protected LatestMessages latestMessages;
/**
* A list of compound marks in the race.
@ -116,16 +111,14 @@ public abstract class Race {
* @param boatDataSource Data source for boat related data (yachts and marker boats).
* @param raceDataSource Data source for race related data (participating boats, legs, etc...).
* @param regattaDataSource Data source for race related data (course name, location, timezone, etc...).
* @param latestMessages The collection of latest messages, which can be written to, or read from.
*/
public Race(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages) {
public Race(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource) {
//Keep a reference to data sources.
this.raceDataSource = raceDataSource;
this.boatDataSource = boatDataSource;
this.regattaDataSource = regattaDataSource;
this.latestMessages = latestMessages;
//Marks.
@ -241,7 +234,7 @@ public abstract class Race {
* @param windBearing New wind bearing.
* @param windSpeedKnots New wind speed, in knots.
*/
protected void setWind(Bearing windBearing, double windSpeedKnots) {
public void setWind(Bearing windBearing, double windSpeedKnots) {
Wind wind = new Wind(windBearing, windSpeedKnots);
setWind(wind);
}
@ -250,7 +243,7 @@ public abstract class Race {
* Updates the race to have a specified wind (bearing and speed).
* @param wind New wind.
*/
protected void setWind(Wind wind) {
public void setWind(Wind wind) {
this.raceWind.setValue(wind);
}
@ -316,6 +309,23 @@ public abstract class Race {
return boundary;
}
/**
* Returns the marks of the race.
* @return Marks of the race.
*/
public List<CompoundMark> getCompoundMarks() {
return compoundMarks;
}
/**
* Returns the legs of the race.
* @return Legs of the race.
*/
public List<Leg> getLegs() {
return legs;
}
/**
* Returns the number of frames generated per second.
* @return Frames per second.

@ -0,0 +1,346 @@
package shared.model;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
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;
/**
* 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() {
//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) {
//We add a "dummy" leg at the end of the race.
if (legs.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());
}
/**
* 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 List<Leg> getLegs() {
return raceDataSource.getLegs();
}
/**
* 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();
}
}

@ -50,4 +50,5 @@ public interface RunnableWithFramePeriod extends Runnable {
}
}

@ -30,8 +30,12 @@ public class ConnectionToServerCommandFactory {
switch(joinAcceptance.getAcceptanceType()) {
case JOIN_SUCCESSFUL: return new JoinSuccessfulCommand(joinAcceptance, connectionToServer);
case RACE_PARTICIPANTS_FULL: return new RaceParticipantsFullCommand(joinAcceptance, connectionToServer);
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.");

@ -5,13 +5,11 @@ 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#RACE_PARTICIPANTS_FULL} {@link JoinAcceptance} message is received.
*/
public class RaceParticipantsFullCommand implements Command {
public class JoinFailureCommand implements Command {
/**
* The message to operate on.
@ -25,11 +23,11 @@ public class RaceParticipantsFullCommand implements Command {
/**
* Creates a new {@link RaceParticipantsFullCommand}, which operates on a given {@link 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 RaceParticipantsFullCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
public JoinFailureCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
this.joinAcceptance = joinAcceptance;
this.connectionToServer = connectionToServer;
}

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

@ -5,13 +5,11 @@ 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#JOIN_SUCCESSFUL} {@link network.Messages.JoinAcceptance} message is received.
* Command created when a {@link network.Messages.Enums.JoinAcceptanceEnum#JOIN_SUCCESSFUL_PARTICIPANT} {@link JoinAcceptance} message is received.
*/
public class JoinSuccessfulCommand implements Command {
public class JoinSuccessSpectatorCommand implements Command {
/**
* The message to operate on.
@ -25,11 +23,11 @@ public class JoinSuccessfulCommand implements Command {
/**
* Creates a new {@link JoinSuccessfulCommand}, which operates on a given {@link 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 JoinSuccessfulCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
public JoinSuccessSpectatorCommand(JoinAcceptance joinAcceptance, ConnectionToServer connectionToServer) {
this.joinAcceptance = joinAcceptance;
this.connectionToServer = connectionToServer;
}

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

@ -4,7 +4,6 @@ package visualiser.Controllers;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
@ -12,7 +11,6 @@ import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
import shared.model.Bearing;
import shared.model.Wind;
import visualiser.model.VisualiserRace;
/**
* Controller for the arrow.fxml view.

@ -4,14 +4,14 @@ import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.layout.AnchorPane;
import visualiser.gameController.ControllerClient;
import visualiser.network.ServerConnection;
import visualiser.model.VisualiserBoat;
import visualiser.model.VisualiserRace;
import visualiser.model.VisualiserRaceEvent;
import java.net.Socket;
import java.net.URL;
import java.util.ResourceBundle;
/**
* Controller that everything is overlayed onto. This makes it so that changing scenes does not resize your stage.
*/
@ -38,10 +38,9 @@ public class MainController extends Controller {
* Transitions from the StartController screen (displays pre-race information) to the RaceController (displays the actual race).
* @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(VisualiserRace visualiserRace, ControllerClient controllerClient, ServerConnection serverConnection) {
raceController.startRace(visualiserRace, controllerClient, serverConnection);
public void beginRace(VisualiserRaceEvent visualiserRace, ControllerClient controllerClient) {
raceController.startRace(visualiserRace, controllerClient);
}
/**

@ -4,7 +4,9 @@ package visualiser.Controllers;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.fxml.FXML;
import javafx.scene.chart.LineChart;
import javafx.scene.control.*;
@ -28,6 +30,8 @@ import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Controller used to display a running race.
*/
@ -37,12 +41,16 @@ public class RaceController extends Controller {
/**
* The race object which describes the currently occurring race.
*/
private VisualiserRace visualiserRace;
private VisualiserRaceEvent visualiserRace;
/**
* An additional observable list of boats. This is used by the table view, to allow it to sort boats without effecting the race's own list of boats.
* Service for sending keystrokes to server
*/
private ObservableList<VisualiserBoat> tableBoatList;
private ControllerClient controllerClient;
/**
* The canvas that draws the race.
@ -59,15 +67,6 @@ public class RaceController extends Controller {
*/
@FXML private ArrowController arrowController;
/**
* Service for sending keystrokes to server
*/
private ControllerClient controllerClient;
/**
* The connection to the server.
*/
private ServerConnection serverConnection;
@FXML private GridPane canvasBase;
@ -162,7 +161,7 @@ public class RaceController extends Controller {
* Initialises the frame rate functionality. This allows for toggling the frame rate, and connect the fps label to the race's fps property.
* @param visualiserRace The race to connect the fps label to.
*/
private void initialiseFps(VisualiserRace visualiserRace) {
private void initialiseFps(VisualiserRaceEvent visualiserRace) {
//On/off toggle.
initialiseFpsToggle();
@ -192,9 +191,9 @@ public class RaceController extends Controller {
* Initialises the fps label to update when the race fps changes.
* @param visualiserRace The race to monitor the frame rate of.
*/
private void initialiseFpsLabel(VisualiserRace visualiserRace) {
private void initialiseFpsLabel(VisualiserRaceEvent visualiserRace) {
visualiserRace.fpsProperty().addListener((observable, oldValue, newValue) -> {
visualiserRace.getFrameRateProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> this.FPS.setText("FPS: " + newValue.toString()));
});
@ -206,14 +205,15 @@ public class RaceController extends Controller {
* Initialises the information table view to listen to a given race.
* @param race Race to listen to.
*/
public void initialiseInfoTable(VisualiserRace race) {
public void initialiseInfoTable(VisualiserRaceEvent race) {
//Copy list of boats.
this.tableBoatList = FXCollections.observableArrayList(race.getBoats());
SortedList<VisualiserBoat> sortedBoats = new SortedList<>(race.getVisualiserRaceState().getBoats());
sortedBoats.comparatorProperty().bind(boatInfoTable.comparatorProperty());
//Set up table.
boatInfoTable.setItems(this.tableBoatList);
boatInfoTable.setItems(sortedBoats);
//Set up each column.
@ -293,12 +293,12 @@ public class RaceController extends Controller {
/**
* Initialises the {@link Sparkline}, and listens to a specified {@link VisualiserRace}.
* Initialises the {@link Sparkline}, and listens to a specified {@link VisualiserRaceEvent}.
* @param race The race to listen to.
*/
private void initialiseSparkline(VisualiserRace race) {
private void initialiseSparkline(VisualiserRaceEvent race) {
//The race.getBoats() we are passing in is sorted by position in race inside the race class.
this.sparkline = new Sparkline(this.visualiserRace, this.sparklineChart);
this.sparkline = new Sparkline(this.visualiserRace.getVisualiserRaceState(), this.sparklineChart);
}
@ -306,7 +306,7 @@ public class RaceController extends Controller {
* Initialises the {@link ResizableRaceCanvas}, provides the race to read data from.
* @param race Race to read data from.
*/
private void initialiseRaceCanvas(VisualiserRace race) {
private void initialiseRaceCanvas(VisualiserRaceEvent race) {
//Create canvas.
raceCanvas = new ResizableRaceCanvas(race);
@ -329,18 +329,18 @@ public class RaceController extends Controller {
* Intialises the race time zone label with the race's time zone.
* @param race The race to get time zone from.
*/
private void initialiseRaceTimezoneLabel(VisualiserRace race) {
timeZone.setText(race.getRaceClock().getTimeZone());
private void initialiseRaceTimezoneLabel(VisualiserRaceEvent race) {
timeZone.setText(race.getVisualiserRaceState().getRaceClock().getTimeZone());
}
/**
* Initialises the race clock to listen to the specified race.
* @param race The race to listen to.
*/
private void initialiseRaceClock(VisualiserRace race) {
private void initialiseRaceClock(VisualiserRaceEvent race) {
//RaceClock.duration isn't necessarily being changed in the javaFX thread, so we need to runlater the update.
race.getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> {
race.getVisualiserRaceState().getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
timer.setText(newValue);
});
@ -353,13 +353,11 @@ public class RaceController extends Controller {
* Displays a specified race.
* @param visualiserRace Object modelling the race.
* @param controllerClient Socket Client that manipulates the controller.
* @param serverConnection The connection to the server.
*/
public void startRace(VisualiserRace visualiserRace, ControllerClient controllerClient, ServerConnection serverConnection) {
public void startRace(VisualiserRaceEvent visualiserRace, ControllerClient controllerClient) {
this.visualiserRace = visualiserRace;
this.controllerClient = controllerClient;
this.serverConnection = serverConnection;
initialiseRace();
@ -385,8 +383,8 @@ public class RaceController extends Controller {
* Initialises the arrow controller with data from the race to observe.
* @param race The race to observe.
*/
private void initialiseArrow(VisualiserRace race) {
arrowController.setWindProperty(race.windProperty());
private void initialiseArrow(VisualiserRaceEvent race) {
arrowController.setWindProperty(race.getVisualiserRaceState().windProperty());
}
@ -399,7 +397,7 @@ public class RaceController extends Controller {
public void handle(long arg0) {
//Get the current race status.
RaceStatusEnum raceStatus = visualiserRace.getRaceStatusEnum();
RaceStatusEnum raceStatus = visualiserRace.getVisualiserRaceState().getRaceStatusEnum();
//If the race has finished, go to finish view.
@ -408,7 +406,7 @@ public class RaceController extends Controller {
stop();
//Hide this, and display the finish controller.
finishRace(visualiserRace.getBoats());
finishRace(visualiserRace.getVisualiserRaceState().getBoats());
} else {
//Otherwise, render the canvas.
@ -421,7 +419,7 @@ public class RaceController extends Controller {
}
//Return to main screen if we lose connection.
if (!serverConnection.isAlive()) {
if (!visualiserRace.getServerConnection().isAlive()) {
race.setVisible(false);
parent.enterTitle();
//TODO currently this doesn't work correctly - the title screen remains visible after clicking join game

@ -9,7 +9,7 @@ import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color;
import mock.model.commandFactory.CompositeCommand;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.Enums.RequestToJoinEnum;
import network.Messages.LatestMessages;
@ -20,9 +20,10 @@ import shared.exceptions.InvalidRaceDataException;
import shared.exceptions.InvalidRegattaDataException;
import shared.exceptions.XMLReaderException;
import visualiser.gameController.ControllerClient;
import visualiser.model.VisualiserRaceState;
import visualiser.network.ServerConnection;
import visualiser.model.VisualiserBoat;
import visualiser.model.VisualiserRace;
import visualiser.model.VisualiserRaceEvent;
import java.io.IOException;
import java.net.Socket;
@ -31,10 +32,11 @@ import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Controller to for waiting for the race to start.
*/
public class StartController extends Controller implements Observer {
public class StartController extends Controller {
@FXML private GridPane start;
@FXML private AnchorPane startWrapper;
@ -70,9 +72,10 @@ public class StartController extends Controller implements Observer {
/**
* Our connection to the server.
* The race + connection to server.
*/
private ServerConnection serverConnection;
private VisualiserRaceEvent visualiserRaceEvent;
/**
* Writes BoatActions to outgoing message queue.
@ -80,27 +83,9 @@ public class StartController extends Controller implements Observer {
private ControllerClient controllerClient;
/**
* The race object which describes the currently occurring race.
*/
private VisualiserRace visualiserRace;
/**
* An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor.
*/
private List<Color> colors = new ArrayList<>(Arrays.asList(
Color.BLUEVIOLET,
Color.BLACK,
Color.RED,
Color.ORANGE,
Color.DARKOLIVEGREEN,
Color.LIMEGREEN,
Color.PURPLE,
Color.DARKGRAY,
Color.YELLOW
));
@ -117,41 +102,17 @@ public class StartController extends Controller implements Observer {
/**
* Starts the race.
* Called once we have received all XML files from the server.
* @param latestMessages The set of latest race messages to use for race.
* @throws XMLReaderException Thrown if XML file cannot be parsed.
* @throws InvalidRaceDataException Thrown if XML file cannot be parsed.
* @throws InvalidBoatDataException Thrown if XML file cannot be parsed.
* @throws InvalidRegattaDataException Thrown if XML file cannot be parsed.
*/
private void startRace(LatestMessages latestMessages) throws XMLReaderException, InvalidRaceDataException, InvalidBoatDataException, InvalidRegattaDataException {
//Create data sources from latest messages for the race.
RaceDataSource raceDataSource = new RaceXMLReader(latestMessages.getRaceXMLMessage().getXmlMessage(), XMLFileType.Contents);
BoatDataSource boatDataSource = new BoatXMLReader(latestMessages.getBoatXMLMessage().getXmlMessage(), XMLFileType.Contents);
RegattaDataSource regattaDataSource = new RegattaXMLReader(latestMessages.getRegattaXMLMessage().getXmlMessage(), XMLFileType.Contents);
//Create race.
this.visualiserRace = new VisualiserRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, this.colors);
new Thread(this.visualiserRace).start();
//Ugly. TODO refactor
ObservableList<VisualiserBoat> boats = visualiserRace.getBoats();
for (VisualiserBoat boat : boats) {
if (boat.getSourceID() == serverConnection.getAllocatedSourceID()) {
boat.setClientBoat(true);
}
}
private void startRace() {
//Initialise the boat table.
initialiseBoatTable(this.visualiserRace);
initialiseBoatTable(this.visualiserRaceEvent.getVisualiserRaceState());
//Initialise the race name.
initialiseRaceName(this.visualiserRace);
initialiseRaceName(this.visualiserRaceEvent.getVisualiserRaceState());
//Initialises the race clock.
initialiseRaceClock(this.visualiserRace);
initialiseRaceClock(this.visualiserRaceEvent.getVisualiserRaceState());
//Starts the race countdown timer.
countdownTimer();
@ -169,7 +130,7 @@ public class StartController extends Controller implements Observer {
* Initialises the boat table that is to be shown on the pane.
* @param visualiserRace The race to get data from.
*/
private void initialiseBoatTable(VisualiserRace visualiserRace) {
private void initialiseBoatTable(VisualiserRaceState visualiserRace) {
//Get the boats.
ObservableList<VisualiserBoat> boats = visualiserRace.getBoats();
@ -184,7 +145,7 @@ public class StartController extends Controller implements Observer {
* Initialises the race name which is shown on the pane.
* @param visualiserRace The race to get data from.
*/
private void initialiseRaceName(VisualiserRace visualiserRace) {
private void initialiseRaceName(VisualiserRaceState visualiserRace) {
raceTitleLabel.setText(visualiserRace.getRegattaName());
@ -194,7 +155,7 @@ public class StartController extends Controller implements Observer {
* Initialises the race clock/timer labels for the start time, current time, and remaining time.
* @param visualiserRace The race to get data from.
*/
private void initialiseRaceClock(VisualiserRace visualiserRace) {
private void initialiseRaceClock(VisualiserRaceState visualiserRace) {
//Start time.
initialiseRaceClockStartTime(visualiserRace);
@ -212,7 +173,7 @@ public class StartController extends Controller implements Observer {
* Initialises the race current time label.
* @param visualiserRace The race to get data from.
*/
private void initialiseRaceClockStartTime(VisualiserRace visualiserRace) {
private void initialiseRaceClockStartTime(VisualiserRaceState visualiserRace) {
raceStartLabel.setText(visualiserRace.getRaceClock().getStartingTimeString());
@ -229,7 +190,7 @@ public class StartController extends Controller implements Observer {
* Initialises the race current time label.
* @param visualiserRace The race to get data from.
*/
private void initialiseRaceClockCurrentTime(VisualiserRace visualiserRace) {
private void initialiseRaceClockCurrentTime(VisualiserRaceState visualiserRace) {
visualiserRace.getRaceClock().currentTimeProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
@ -243,7 +204,7 @@ public class StartController extends Controller implements Observer {
* Initialises the race duration label.
* @param visualiserRace The race to get data from.
*/
private void initialiseRaceClockDuration(VisualiserRace visualiserRace) {
private void initialiseRaceClockDuration(VisualiserRaceState visualiserRace) {
visualiserRace.getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
@ -261,13 +222,13 @@ public class StartController extends Controller implements Observer {
@Override
public void handle(long arg0) {
//TODO instead of having an AnimationTimer checking the race status, we could provide a Property<RaceStatusEnum>, and connect a listener to that.
//Get the current race status.
RaceStatusEnum raceStatus = visualiserRace.getRaceStatusEnum();
RaceStatusEnum raceStatus = visualiserRaceEvent.getVisualiserRaceState().getRaceStatusEnum();
//Display it.
raceStatusLabel.setText("Race Status: " + raceStatus.name());
//If the race has reached the preparatory phase, or has started...
if (raceStatus == RaceStatusEnum.PREPARATORY || raceStatus == RaceStatusEnum.STARTED) {
//Stop this timer.
@ -275,9 +236,10 @@ public class StartController extends Controller implements Observer {
//Hide this, and display the race controller.
startWrapper.setVisible(false);
start.setVisible(false);
//start.setVisible(false);//TODO is this needed?
parent.beginRace(visualiserRaceEvent, controllerClient);
parent.beginRace(visualiserRace, controllerClient, serverConnection);
}
}
}.start();
@ -285,58 +247,25 @@ public class StartController extends Controller implements Observer {
/**
* Function to handle changes in objects we observe.
* We observe LatestMessages.
* @param o The observed object.
* @param arg The {@link Observable#notifyObservers(Object)} parameter.
*/
@Override
public void update(Observable o, Object arg) {
//Check that we actually have LatestMessages.
if (o instanceof LatestMessages) {
LatestMessages latestMessages = (LatestMessages) o;
//If we've received all of the xml files, start the race. Only start it if it hasn't already been created.
if (latestMessages.hasAllXMLMessages() && this.visualiserRace == null) {
//Need to handle it in the javafx thread.
Platform.runLater(() -> {
try {
this.startRace(latestMessages);
} catch (XMLReaderException | InvalidBoatDataException | InvalidRaceDataException | InvalidRegattaDataException e) {
//We currently don't handle this in meaningful way, as it should never occur.
//If we reach this point it means that malformed XML files were sent.
e.printStackTrace();
}
});
}
}
}
/**
* Show starting information for a race given a socket.
* @param socket network source of information
*/
public void enterLobby(Socket socket) {
startWrapper.setVisible(true);
try {
LatestMessages latestMessages = new LatestMessages();
this.serverConnection = new ServerConnection(socket, latestMessages, RequestToJoinEnum.PARTICIPANT);
this.controllerClient = serverConnection.getControllerClient();
this.visualiserRaceEvent = new VisualiserRaceEvent(socket, RequestToJoinEnum.PARTICIPANT);
this.controllerClient = visualiserRaceEvent.getControllerClient();
//Store a reference to latestMessages so that we can observe it.
latestMessages.addObserver(this);
new Thread(this.serverConnection).start();
startWrapper.setVisible(true);
startRace();
} catch (IOException e) {
Logger.getGlobal().log(Level.WARNING, "Could not connection to server.", e);
//TODO should probably let this propagate, so that we only enter this scene if everything works
Logger.getGlobal().log(Level.WARNING, "Could not connect to server.", e);
}
}

@ -11,22 +11,22 @@ public class RaceMap {
/**
* The longitude of the left side of the map.
*/
private final double longLeft;
private double longLeft;
/**
* The longitude of the right side of the map.
*/
private final double longRight;
private double longRight;
/**
* The latitude of the top side of the map.
*/
private final double latTop;
private double latTop;
/**
* The latitude of the bottom side of the map.
*/
private final double latBottom;
private double latBottom;
/**
@ -143,4 +143,27 @@ public class RaceMap {
public void setHeight(int height) {
this.height = height;
}
/**
* Updates the bottom right GPS coordinates of the RaceMap.
* @param bottomRight New bottom right GPS coordinates.
*/
public void setGPSBotRight(GPSCoordinate bottomRight) {
this.latBottom = bottomRight.getLatitude();
this.longRight = bottomRight.getLongitude();
}
/**
* Updates the top left GPS coordinates of the RaceMap.
* @param topLeft New top left GPS coordinates.
*/
public void setGPSTopLeft(GPSCoordinate topLeft) {
this.latTop = topLeft.getLatitude();
this.longLeft = topLeft.getLongitude();
}
}

@ -1,8 +1,6 @@
package visualiser.model;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.transform.Rotate;
@ -12,6 +10,7 @@ import shared.model.GPSCoordinate;
import shared.model.Mark;
import shared.model.RaceClock;
import java.util.ArrayList;
import java.util.List;
/**
@ -36,7 +35,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
/**
* The race we read data from and draw.
*/
private VisualiserRace visualiserRace;
private VisualiserRaceEvent visualiserRace;
private boolean annoName = true;
@ -49,15 +48,15 @@ public class ResizableRaceCanvas extends ResizableCanvas {
/**
* Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRace}.
* Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRaceEvent}.
* @param visualiserRace The race that data is read from in order to be drawn.
*/
public ResizableRaceCanvas(VisualiserRace visualiserRace) {
public ResizableRaceCanvas(VisualiserRaceEvent visualiserRace) {
super();
this.visualiserRace = visualiserRace;
RaceDataSource raceData = visualiserRace.getRaceDataSource();
RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource();
double lat1 = raceData.getMapTopLeft().getLatitude();
double long1 = raceData.getMapTopLeft().getLongitude();
@ -264,8 +263,8 @@ public class ResizableRaceCanvas extends ResizableCanvas {
boat.getCountry(),
boat.getCurrentSpeed(),
this.map.convertGPS(boat.getCurrentPosition()),
boat.getTimeToNextMarkFormatted(this.visualiserRace.getRaceClock().getCurrentTime()),
boat.getTimeSinceLastMarkFormatted(this.visualiserRace.getRaceClock().getCurrentTime()) );
boat.getTimeToNextMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()),
boat.getTimeSinceLastMarkFormatted(this.visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime()) );
}
@ -277,7 +276,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
*/
private void drawBoats() {
for (VisualiserBoat boat : visualiserRace.getBoats()) {
for (VisualiserBoat boat : new ArrayList<>(visualiserRace.getVisualiserRaceState().getBoats())) {
//Draw the boat.
drawBoat(boat);
@ -290,7 +289,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
//If the race hasn't started, we set the time since last mark to the current time, to ensure we don't start counting until the race actually starts.
if ((boat.getStatus() != BoatStatusEnum.RACING) && (boat.getStatus() == BoatStatusEnum.FINISHED)) {
boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime());
boat.setTimeAtLastMark(visualiserRace.getVisualiserRaceState().getRaceClock().getCurrentTime());
}
//Draw boat label.
@ -402,7 +401,8 @@ public class ResizableRaceCanvas extends ResizableCanvas {
* Draws all of the {@link Mark}s on the canvas.
*/
private void drawMarks() {
for (Mark mark : this.visualiserRace.getMarks()) {
for (Mark mark : new ArrayList<>(visualiserRace.getVisualiserRaceState().getMarks())) {
drawMark(mark);
}
}
@ -462,7 +462,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
//Calculate the screen coordinates of the boundary.
List<GPSCoordinate> boundary = this.visualiserRace.getBoundary();
List<GPSCoordinate> boundary = new ArrayList<>(visualiserRace.getVisualiserRaceState().getBoundary());
double[] xpoints = new double[boundary.size()];
double[] ypoints = new double[boundary.size()];
@ -486,6 +486,10 @@ public class ResizableRaceCanvas extends ResizableCanvas {
*/
public void drawRace() {
//Update RaceMap with new GPS values of race.
this.map.setGPSTopLeft(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapTopLeft());
this.map.setGPSBotRight(visualiserRace.getVisualiserRaceState().getRaceDataSource().getMapBottomRight());
gc.setLineWidth(2);
clear();
@ -520,7 +524,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
gc.setFill(boat.getColor());
//Draw each TrackPoint.
for (TrackPoint point : boat.getTrack()) {
for (TrackPoint point : new ArrayList<>(boat.getTrack())) {
//Convert the GPSCoordinate to a screen coordinate.
GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate());

@ -2,6 +2,7 @@ package visualiser.model;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
@ -23,7 +24,7 @@ public class Sparkline {
/**
* The race to observe.
*/
private VisualiserRace race;
private VisualiserRaceState race;
/**
* The boats to observe.
@ -58,9 +59,9 @@ public class Sparkline {
* @param race The race to listen to.
* @param sparklineChart JavaFX LineChart for the sparkline.
*/
public Sparkline(VisualiserRace race, LineChart<Number,Number> sparklineChart) {
public Sparkline(VisualiserRaceState race, LineChart<Number,Number> sparklineChart) {
this.race = race;
this.boats = race.getBoats();
this.boats = new SortedList<>(race.getBoats());
this.legNum = race.getLegCount();
this.sparklineChart = sparklineChart;

@ -174,6 +174,7 @@ public class VisualiserBoat extends Boat {
*/
public String getTimeToNextMarkFormatted(ZonedDateTime currentTime) {
if (getTimeAtLastMark() != null) {
//Calculate time delta.
Duration timeUntil = Duration.between(currentTime, getEstimatedTimeAtNextMark());
@ -198,6 +199,10 @@ public class VisualiserBoat extends Boat {
}
} else {
return " -";
}
}

@ -1,472 +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.
if (latestMessages.getRaceStatus() != null) {
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;
}
}

@ -1,6 +1,11 @@
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;
@ -8,8 +13,7 @@ import java.util.logging.Logger;
/**
* TCP client which receives packets/messages from a race data source
* (e.g., mock source, official source), and exposes them to any observers.
* The controller for race related messages, coming from the server to the client.
*/
public class VisualiserRaceController implements RunnableWithFramePeriod {
@ -21,31 +25,29 @@ public class VisualiserRaceController implements RunnableWithFramePeriod {
/**
* An object containing the set of latest messages to write to.
* Every server frame, VisualiserInput reads messages from its incomingMessages, and write them to this.
* Commands are placed in here, and executed by visualiserRace.
*/
private LatestMessages latestMessages;
private CompositeCommand compositeRaceCommand;
/**
* Constructs a visualiserInput to convert an incoming stream of messages into LatestMessages.
*
* @param latestMessages Object to place messages in.
* @param incomingMessages The incoming queue of messages.
* The context that created commands operate on.
*/
public VisualiserRaceController(LatestMessages latestMessages, BlockingQueue<AC35Data> incomingMessages) {
this.latestMessages = latestMessages;
this.incomingMessages = incomingMessages;
}
private VisualiserRaceState visualiserRace;
/**
* Returns the LatestMessages object, which can be queried for any received race related messages.
*
* @return The LatestMessages object.
* 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 LatestMessages getLatestMessages() {
return latestMessages;
public VisualiserRaceController(BlockingQueue<AC35Data> incomingMessages, VisualiserRaceState visualiserRace, CompositeCommand compositeRaceCommand) {
this.incomingMessages = incomingMessages;
this.compositeRaceCommand = compositeRaceCommand;
this.visualiserRace = visualiserRace;
}
@ -56,157 +58,21 @@ public class VisualiserRaceController implements RunnableWithFramePeriod {
while (!Thread.interrupted()) {
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()) {
//RaceStatus.
case RACESTATUS: {
RaceStatus raceStatus = (RaceStatus) message;
//System.out.println("Race Status Message");
this.latestMessages.setRaceStatus(raceStatus);
AC35Data message = incomingMessages.take();
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;
}
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;
}
//Main loop.
// take message
// create command
// place in command queue
}
}

@ -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,411 @@
package visualiser.model;
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.
*/
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);
}
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) {
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);
}
/**
* See {@link RaceState#useLegsList(List)}.
* Also initialises the {@link #legCompletionOrder} map.
* @param legs The new list of legs to use.
*/
@Override
public void useLegsList(List<Leg> legs) {
super.useLegsList(legs);
//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 : 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();
}
}

@ -147,11 +147,27 @@ public class ConnectionToServer implements RunnableWithFramePeriod {
//Send them the source ID.
RequestToJoin requestToJoin = new RequestToJoin(requestType);
outgoingMessages.put(requestToJoin);
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;
}
}

@ -2,18 +2,20 @@ 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.JoinAcceptance;
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;
@ -23,7 +25,7 @@ import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class handles the client-server connection handshake, and creation of VisualiserInput and ControllerClient.
* This class handles the client-server connection handshake.
*/
public class ServerConnection implements RunnableWithFramePeriod {
@ -32,16 +34,8 @@ public class ServerConnection implements RunnableWithFramePeriod {
*/
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.
*/
private LatestMessages latestMessages;
/**
@ -122,6 +116,16 @@ public class ServerConnection implements RunnableWithFramePeriod {
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.
@ -130,7 +134,7 @@ public class ServerConnection implements RunnableWithFramePeriod {
/**
* The thread {@link #visualiserRaceController} runs on.
*/
private Thread visualiserInputThread;
private Thread visualiserRaceControllerThread;
@ -138,13 +142,15 @@ public class ServerConnection implements RunnableWithFramePeriod {
/**
* Creates a server connection, using a given socket.
* @param socket The socket which connects to the client.
* @param latestMessages Latest race snapshot to send to 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, LatestMessages latestMessages, RequestToJoinEnum requestType) throws IOException {
public ServerConnection(Socket socket, VisualiserRaceState visualiserRaceState, CompositeCommand raceCommands, RequestToJoinEnum requestType) throws IOException {
this.socket = socket;
this.latestMessages = latestMessages;
this.visualiserRaceState = visualiserRaceState;
this.raceCommands = raceCommands;
createMessageSerialiser(socket);
createMessageDeserialiser(socket);
@ -205,6 +211,15 @@ public class ServerConnection implements RunnableWithFramePeriod {
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.
@ -234,7 +249,7 @@ public class ServerConnection implements RunnableWithFramePeriod {
/**
* Creates the {@link #heartBeatService} and {@link #heartBeatController} and starts its thread.
* Creates the {@link #heartBeatService} and {@link #heartBeatController} and starts their threads.
*/
private void createHeartBeatService() {
@ -260,14 +275,16 @@ public class ServerConnection implements RunnableWithFramePeriod {
}
/**
* Creates the {@link #visualiserRaceController} and starts its thread.
*/
private void createVisualiserRaceController() {
private void createVisualiserRace() {
//VisualiserRaceController receives messages, and places commands on the race's command queue.
BlockingQueue<AC35Data> incomingMessages = new LinkedBlockingQueue<>();
this.visualiserRaceController = new VisualiserRaceController(incomingMessages, visualiserRaceState, raceCommands);
this.visualiserRaceController = new VisualiserRaceController(latestMessages, incomingMessages);
this.visualiserInputThread = new Thread(visualiserRaceController, "ServerConnection()->VisualiserInput thread " + visualiserRaceController);
this.visualiserInputThread.start();
//Routes.
this.messageRouter.addRoute(MessageType.BOATLOCATION, incomingMessages);
@ -281,15 +298,19 @@ public class ServerConnection implements RunnableWithFramePeriod {
this.messageRouter.addRoute(MessageType.YACHTEVENTCODE, incomingMessages);
this.messageRouter.addRoute(MessageType.MARKROUNDING, incomingMessages);
this.messageRouter.addRoute(MessageType.XMLMESSAGE, incomingMessages);
this.messageRouter.removeDefaultRoute();
this.messageRouter.addRoute(MessageType.ASSIGN_PLAYER_BOAT, incomingMessages);
this.messageRouter.removeDefaultRoute(); //We no longer want to keep un-routed messages.
//TODO create VisualiserRace here or somewhere else?
//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());
@ -342,16 +363,17 @@ public class ServerConnection implements RunnableWithFramePeriod {
*/
private void connected() {
JoinAcceptance joinAcceptance = connectionToServer.getJoinAcceptance();
allocatedSourceID = joinAcceptance.getSourceID();
createHeartBeatService();
createVisualiserRace();
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.
@ -403,13 +425,7 @@ public class ServerConnection implements RunnableWithFramePeriod {
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;
}
/**
@ -447,10 +463,12 @@ public class ServerConnection implements RunnableWithFramePeriod {
}
if (this.visualiserInputThread != null) {
this.visualiserInputThread.interrupt();
if (this.visualiserRaceControllerThread != null) {
this.visualiserRaceControllerThread.interrupt();
}
//TODO visualiser race?
//TODO input controller?
}

@ -49,7 +49,7 @@
<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">
<Boat BoatName="ORACLE TEAM USA" HullNum="RG01" ShapeID="0" ShortName="USA" SourceID="126" StoweName="USA" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
</Boats>

@ -3,9 +3,25 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.chart.LineChart?>
<?import javafx.scene.chart.NumberAxis?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.Accordion?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.RadioButton?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.SplitPane?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.control.ToggleGroup?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.text.Font?>
<SplitPane fx:id="race" dividerPositions="0.7" prefHeight="431.0" prefWidth="610.0" visible="false" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.RaceController">
<items>
<GridPane fx:id="canvasBase">
@ -87,8 +103,8 @@
<TableView fx:id="boatInfoTable" layoutX="-2.0" prefHeight="265.0" prefWidth="242.0" AnchorPane.bottomAnchor="164.0" AnchorPane.leftAnchor="-2.0" AnchorPane.rightAnchor="-62.0" AnchorPane.topAnchor="0.0">
<columns>
<TableColumn fx:id="boatPlacingColumn" prefWidth="50.0" text="Place" />
<TableColumn fx:id="boatTeamColumn" prefWidth="100.0" text="Team" />
<TableColumn fx:id="boatMarkColumn" prefWidth="130.0" text="Mark" />
<TableColumn fx:id="boatTeamColumn" prefWidth="200.0" text="Team" />
<TableColumn fx:id="boatMarkColumn" prefWidth="150.0" text="Mark" />
<TableColumn fx:id="boatSpeedColumn" prefWidth="75.0" text="Speed" />
</columns>
</TableView>

@ -29,13 +29,12 @@ public class MockRaceTest {
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);
MockRace mockRace = new MockRace(boatDataSource, raceDataSource, regattaDataSource, polars, Constants.RaceTimeScale, windGenerator);
return mockRace;

@ -8,16 +8,11 @@ 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;

@ -64,7 +64,7 @@ public class JoinAcceptanceDecoderTest {
*/
@Test
public void joinSuccessSourceIDTest() throws Exception {
responseTypeTest(JoinAcceptanceEnum.JOIN_SUCCESSFUL, 12345);
responseTypeTest(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, 12345);
}
/**
@ -73,26 +73,9 @@ public class JoinAcceptanceDecoderTest {
*/
@Test
public void joinSuccessNoSourceIDTest() throws Exception {
responseTypeTest(JoinAcceptanceEnum.JOIN_SUCCESSFUL, 0);
responseTypeTest(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, 0);
}
/**
* Tests if a participants full message can be encoded and decoded correctly.
* @throws Exception if test fails.
*/
@Test
public void participantFullTest() throws Exception {
responseTypeTest(JoinAcceptanceEnum.RACE_PARTICIPANTS_FULL, 0);
}
/**
* Tests if a ghosts full message can be encoded and decoded correctly.
* @throws Exception if test fails.
*/
@Test
public void ghostFullTest() throws Exception {
responseTypeTest(JoinAcceptanceEnum.GHOST_PARTICIPANTS_FULL, 0);
}
/**
* Tests if a server full message can be encoded and decoded correctly.

@ -7,13 +7,15 @@ import network.Messages.Enums.RequestToJoinEnum;
import network.Messages.JoinAcceptance;
import org.junit.Before;
import org.junit.Test;
import visualiser.Commands.ConnectionToServerCommands.JoinSuccessfulCommand;
import visualiser.Commands.ConnectionToServerCommands.RaceParticipantsFullCommand;
import visualiser.Commands.ConnectionToServerCommands.JoinSuccessParticipantCommand;
import visualiser.Commands.ConnectionToServerCommands.JoinFailureCommand;
import visualiser.Commands.ConnectionToServerCommands.ServerFullCommand;
import visualiser.enums.ConnectionToServerState;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.junit.Assert.*;
@ -50,7 +52,7 @@ public class ConnectionToServerParticipantTest {
public void expectRequestSent() throws Exception {
//Need to wait for connection thread to execute commands.
Thread.sleep(20);
Thread.sleep(250);
assertEquals(ConnectionToServerState.REQUEST_SENT, connectionToServer.getConnectionState());
}
@ -64,9 +66,14 @@ public class ConnectionToServerParticipantTest {
public void interruptTimedOut() throws Exception {
//Need to wait for connection thread to execute commands.
Thread.sleep(20);
Thread.sleep(250);
//Disable logging as we know this will log but we don't care.
Logger.getGlobal().setLevel(Level.OFF);
connectionToServerThread.interrupt();
Logger.getGlobal().setLevel(null);
connectionToServerThread.join();
assertEquals(ConnectionToServerState.TIMED_OUT, connectionToServer.getConnectionState());
@ -80,20 +87,20 @@ public class ConnectionToServerParticipantTest {
@Test
public void sendJoinSuccessCommand() throws Exception {
int sourceID = 123;
JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID);
JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, sourceID);
Command command = new JoinSuccessfulCommand(joinAcceptance, connectionToServer);
Command command = new JoinSuccessParticipantCommand(joinAcceptance, connectionToServer);
incomingCommands.put(command);
//Need to wait for connection thread to execute commands.
Thread.sleep(20);
Thread.sleep(250);
assertEquals(ConnectionToServerState.CONNECTED, connectionToServer.getConnectionState());
assertTrue(connectionToServer.getJoinAcceptance() != null);
assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID());
assertNotEquals(0, connectionToServer.getJoinAcceptance().getSourceID());
assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL, connectionToServer.getJoinAcceptance().getAcceptanceType());
assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, connectionToServer.getJoinAcceptance().getAcceptanceType());
}
@ -104,21 +111,21 @@ public class ConnectionToServerParticipantTest {
* @throws Exception On error.
*/
@Test
public void sendRaceParticipantsFullCommand() throws Exception {
public void sendJoinFailureCommand() throws Exception {
int sourceID = 0;
JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.RACE_PARTICIPANTS_FULL, sourceID);
JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_FAILURE, sourceID);
Command command = new RaceParticipantsFullCommand(joinAcceptance, connectionToServer);
Command command = new JoinFailureCommand(joinAcceptance, connectionToServer);
incomingCommands.put(command);
//Need to wait for connection thread to execute commands.
Thread.sleep(20);
Thread.sleep(250);
assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState());
assertTrue(connectionToServer.getJoinAcceptance() != null);
assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID());
assertEquals(JoinAcceptanceEnum.RACE_PARTICIPANTS_FULL, connectionToServer.getJoinAcceptance().getAcceptanceType());
assertEquals(JoinAcceptanceEnum.JOIN_FAILURE, connectionToServer.getJoinAcceptance().getAcceptanceType());
}
@ -136,7 +143,7 @@ public class ConnectionToServerParticipantTest {
incomingCommands.put(command);
//Need to wait for connection thread to execute commands.
Thread.sleep(20);
Thread.sleep(250);
assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState());
assertTrue(connectionToServer.getJoinAcceptance() != null);

@ -7,8 +7,7 @@ import network.Messages.Enums.RequestToJoinEnum;
import network.Messages.JoinAcceptance;
import org.junit.Before;
import org.junit.Test;
import visualiser.Commands.ConnectionToServerCommands.JoinSuccessfulCommand;
import visualiser.Commands.ConnectionToServerCommands.RaceParticipantsFullCommand;
import visualiser.Commands.ConnectionToServerCommands.JoinSuccessParticipantCommand;
import visualiser.Commands.ConnectionToServerCommands.ServerFullCommand;
import visualiser.enums.ConnectionToServerState;
@ -50,7 +49,7 @@ public class ConnectionToServerSpectatorTest {
public void expectRequestSent() throws Exception {
//Need to wait for connection thread to execute commands.
Thread.sleep(20);
Thread.sleep(250);
assertEquals(ConnectionToServerState.REQUEST_SENT, connectionToServer.getConnectionState());
}
@ -63,19 +62,19 @@ public class ConnectionToServerSpectatorTest {
@Test
public void sendJoinSuccessCommand() throws Exception {
int sourceID = 0;
JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL, sourceID);
JoinAcceptance joinAcceptance = new JoinAcceptance(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, sourceID);
Command command = new JoinSuccessfulCommand(joinAcceptance, connectionToServer);
Command command = new JoinSuccessParticipantCommand(joinAcceptance, connectionToServer);
incomingCommands.put(command);
//Need to wait for connection thread to execute commands.
Thread.sleep(20);
Thread.sleep(250);
assertEquals(ConnectionToServerState.CONNECTED, connectionToServer.getConnectionState());
assertTrue(connectionToServer.getJoinAcceptance() != null);
assertEquals(sourceID, connectionToServer.getJoinAcceptance().getSourceID());
assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL, connectionToServer.getJoinAcceptance().getAcceptanceType());
assertEquals(JoinAcceptanceEnum.JOIN_SUCCESSFUL_PARTICIPANT, connectionToServer.getJoinAcceptance().getAcceptanceType());
}
@ -95,7 +94,7 @@ public class ConnectionToServerSpectatorTest {
incomingCommands.put(command);
//Need to wait for connection thread to execute commands.
Thread.sleep(20);
Thread.sleep(250);
assertEquals(ConnectionToServerState.DECLINED, connectionToServer.getConnectionState());
assertTrue(connectionToServer.getJoinAcceptance() != null);

Loading…
Cancel
Save