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]
This commit is contained in:
fjc40
2017-08-14 00:53:05 +12:00
parent 89b0aa8b77
commit 7366aba5ec
53 changed files with 2498 additions and 1129 deletions
@@ -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,28 +174,33 @@ public class VisualiserBoat extends Boat {
*/
public String getTimeToNextMarkFormatted(ZonedDateTime currentTime) {
//Calculate time delta.
Duration timeUntil = Duration.between(currentTime, getEstimatedTimeAtNextMark());
if (getTimeAtLastMark() != null) {
//Calculate time delta.
Duration timeUntil = Duration.between(currentTime, getEstimatedTimeAtNextMark());
//Convert to seconds.
long secondsUntil = timeUntil.getSeconds();
//Convert to seconds.
long secondsUntil = timeUntil.getSeconds();
//This means the estimated time is in the past, or not racing.
if ((secondsUntil < 0) || (getStatus() != BoatStatusEnum.RACING)) {
return " -";
}
//This means the estimated time is in the past, or not racing.
if ((secondsUntil < 0) || (getStatus() != BoatStatusEnum.RACING)) {
return " -";
}
if (secondsUntil <= 60) {
//If less than 1 minute, display seconds only.
return " " + secondsUntil + "s";
if (secondsUntil <= 60) {
//If less than 1 minute, display seconds only.
return " " + secondsUntil + "s";
} else {
//Otherwise display minutes and seconds.
long seconds = secondsUntil % 60;
long minutes = (secondsUntil - seconds) / 60;
return String.format(" %dm %ds", minutes, seconds);
}
} else {
//Otherwise display minutes and seconds.
long seconds = secondsUntil % 60;
long minutes = (secondsUntil - seconds) / 60;
return String.format(" %dm %ds", minutes, seconds);
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.
* The context that created commands operate on.
*/
private VisualiserRaceState visualiserRace;
/**
* Constructs a visualiserInput to convert an incoming stream of messages into commands.
* @param incomingMessages The incoming queue of messages.
* @param visualiserRace The context to for commands to operate on.
* @param compositeRaceCommand The composite command to place command in.
*/
public VisualiserRaceController(LatestMessages latestMessages, BlockingQueue<AC35Data> incomingMessages) {
this.latestMessages = latestMessages;
public VisualiserRaceController(BlockingQueue<AC35Data> incomingMessages, VisualiserRaceState visualiserRace, CompositeCommand compositeRaceCommand) {
this.incomingMessages = incomingMessages;
}
/**
* Returns the LatestMessages object, which can be queried for any received race related messages.
*
* @return The LatestMessages object.
*/
public LatestMessages getLatestMessages() {
return latestMessages;
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();
AC35Data message = incomingMessages.take();
Command command = VisualiserRaceCommandFactory.create(message, visualiserRace);
compositeRaceCommand.addCommand(command);
} catch (CommandConstructionException e) {
Logger.getGlobal().log(Level.WARNING, "VisualiserRaceController could not create a command for incoming message.");
} catch (InterruptedException e) {
Logger.getGlobal().log(Level.SEVERE, "VisualiserInput was interrupted on thread: " + Thread.currentThread() + " while waiting for messages.");
Logger.getGlobal().log(Level.SEVERE, "VisualiserRaceController 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);
for (BoatStatus boatStatus : raceStatus.getBoatStatuses()) {
this.latestMessages.setBoatStatus(boatStatus);
}
break;
}
//DisplayTextMessage.
case DISPLAYTEXTMESSAGE: {
//System.out.println("Display Text Message");
//No decoder for this.
break;
}
//XMLMessage.
case XMLMESSAGE: {
XMLMessage xmlMessage = (XMLMessage) message;
//System.out.println("XML Message!");
this.latestMessages.setXMLMessage(xmlMessage);
break;
}
//RaceStartStatus.
case RACESTARTSTATUS: {
//System.out.println("Race Start Status Message");
break;
}
//YachtEventCode.
case YACHTEVENTCODE: {
//YachtEventCode yachtEventCode = (YachtEventCode) message;
//System.out.println("Yacht Event Code!");
//No decoder for this.
break;
}
//YachtActionCode.
case YACHTACTIONCODE: {
//YachtActionCode yachtActionCode = (YachtActionCode) message;
//System.out.println("Yacht Action Code!");
// No decoder for this.
break;
}
//ChatterText.
case CHATTERTEXT: {
//ChatterText chatterText = (ChatterText) message;
//System.out.println("Chatter Text Message!");
//No decoder for this.
break;
}
//BoatLocation.
case BOATLOCATION: {
BoatLocation boatLocation = (BoatLocation) message;
//System.out.println("Boat Location!");
BoatLocation existingBoatLocation = this.latestMessages.getBoatLocationMap().get(boatLocation.getSourceID());
if (existingBoatLocation != null) {
//If our boatlocation map already contains a boat location message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
if (boatLocation.getTime() > existingBoatLocation.getTime()) {
//If it is, replace the old message.
this.latestMessages.setBoatLocation(boatLocation);
}
} else {
//If the map _doesn't_ already contain a message for this boat, insert the message.
this.latestMessages.setBoatLocation(boatLocation);
}
break;
}
//MarkRounding.
case MARKROUNDING: {
MarkRounding markRounding = (MarkRounding) message;
//System.out.println("Mark Rounding Message!");
MarkRounding existingMarkRounding = this.latestMessages.getMarkRoundingMap().get(markRounding.getSourceID());
if (existingMarkRounding != null) {
//If our markRoundingMap already contains a mark rounding message for this boat, check that the new message is actually for a later timestamp (i.e., newer).
if (markRounding.getTime() > existingMarkRounding.getTime()) {
//If it is, replace the old message.
this.latestMessages.setMarkRounding(markRounding);
}
} else {
//If the map _doesn't_ already contain a message for this boat, insert the message.
this.latestMessages.setMarkRounding(markRounding);
}
break;
}
//CourseWinds.
case COURSEWIND: {
//System.out.println("Course Wind Message!");
CourseWinds courseWinds = (CourseWinds) message;
this.latestMessages.setCourseWinds(courseWinds);
break;
}
}
//Main loop.
// take message
// create command
// place in command queue
}
}
@@ -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();
}
}