Merge branch 'master' into story_64_sails

# Conflicts:
#	racevisionGame/src/main/java/mock/app/MockOutput.java
#	racevisionGame/src/main/java/mock/model/MockRace.java
#	racevisionGame/src/main/java/mock/model/commandFactory/CommandFactory.java
#	racevisionGame/src/main/java/network/MessageDecoders/BoatActionDecoder.java
#	racevisionGame/src/main/java/network/MessageDecoders/BoatLocationDecoder.java
#	racevisionGame/src/main/java/visualiser/app/VisualiserInput.java
#	racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java
#	racevisionGame/src/main/java/visualiser/model/ResizableRaceCanvas.java
#	racevisionGame/src/main/java/visualiser/model/VisualiserRace.java
#	racevisionGame/src/main/resources/visualiser/scenes/connect.fxml
#	racevisionGame/src/main/resources/visualiser/scenes/lobby.fxml
#	racevisionGame/src/test/java/mock/model/MockRaceTest.java
#	racevisionGame/src/test/java/network/Utils/AC35UnitConverterTest.java
#	racevisionGame/src/test/java/network/XMLMessageEncoderTest.java
This commit is contained in:
Joseph
2017-08-17 09:13:49 +12:00
184 changed files with 12652 additions and 4461 deletions
@@ -40,6 +40,7 @@ public class Annotations {
private static String pathCheckAnno = "showBoatPath";
private static String timeCheckAnno = "showTime";
private static String estTimeCheckAnno = "showEstTime";
private static String guideLineAnno = "showGuideline";
// string values match the fx:id value of radio buttons
private static String noBtn = "noBtn";
@@ -160,6 +161,16 @@ public class Annotations {
raceMap.draw();
}
});
//listener to show estimated time for annotation
checkBoxes.get(guideLineAnno).selectedProperty()
.addListener((ov, old_val, new_val) -> {
if (old_val != new_val) {
raceMap.toggleGuideLine();
storeCurrentAnnotationState(guideLineAnno, new_val);
raceMap.draw();
}
});
}
/**
@@ -19,16 +19,14 @@ public class Ping implements Runnable {
this.rc = rc;
}
public boolean pingPort(){
//TODO the connection needs to be moved to its own thread, so it doesn't block fx thread.
public boolean pingPort() {
InetSocketAddress i = new InetSocketAddress(hostname, port);
try (Socket s = new Socket()){
s.connect(i, 750);//TODO this should be at least a second or two, once moved to its own thread
s.connect(i, 1500);
s.shutdownInput();
s.shutdownOutput();
s.close();
rc.statusProperty().set("Ready");
//System.out.println(String.valueOf(s.isClosed()));
return true;
} catch (IOException e) {
rc.statusProperty().set("Offline");
@@ -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();
}
}
@@ -10,6 +10,7 @@ import shared.dataInput.RaceDataSource;
import shared.enums.RoundingType;
import shared.model.*;
import java.util.ArrayList;
import java.util.List;
/**
@@ -27,29 +28,34 @@ import java.util.List;
public class ResizableRaceCanvas extends ResizableCanvas {
private RaceMap map; // for converting GPSCoordinates to GraphCoordinates
private VisualiserRace visualiserRace; // draw data read from this race
private Image background;
private Image sailsRight = new Image("/images/sailsRight.png");
private Image sailsLeft = new Image("/images/sailsLeft.png");
private Image sailsLuff = new Image("/images/sailsLuff.gif", 25, 10, false, false);
// annotations
/**
* The race we read data from and draw.
*/
private VisualiserRaceEvent visualiserRace;
private boolean annoName = true;
private boolean annoAbbrev = true;
private boolean annoSpeed = true;
private boolean annoPath = true;
private boolean annoEstTime = true;
private boolean annoTimeSinceLastMark = true;
private boolean annoGuideLine = false;
/**
* 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();
@@ -99,6 +105,16 @@ public class ResizableRaceCanvas extends ResizableCanvas {
annoSpeed = !annoSpeed;
}
/**
* Toggle the guideline annotation
*/
public void toggleGuideLine() {
annoGuideLine = !annoGuideLine;
}
/**
* Rotates things displayed on the canvas. Note: this must be called in
* between gc.save() and gc.restore() else they will rotate everything
@@ -241,18 +257,19 @@ 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()) );
}
/**
* Draws all of the boats on the canvas.
*/
private void drawBoats() {
for (VisualiserBoat boat : visualiserRace.getBoats()) {
//Draw track.
drawTrack(boat);
for (VisualiserBoat boat : new ArrayList<>(visualiserRace.getVisualiserRaceState().getBoats())) {
//Draw the boat.
drawBoat(boat);
//Only draw wake if they are currently racing.
if (boat.getStatus() == BoatStatusEnum.RACING) {
@@ -264,7 +281,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.
@@ -278,40 +295,71 @@ public class ResizableRaceCanvas extends ResizableCanvas {
*/
private void drawBoat(VisualiserBoat boat) {
//The position may be null if we haven't received any BoatLocation messages yet.
if (boat.getCurrentPosition() != null) {
if (boat.isClientBoat()) {
drawClientBoat(boat);
}
//Convert position to graph coordinate.
GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
//Convert position to graph coordinate.
GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
//The x coordinates of each vertex of the boat.
double[] x = {
pos.getX() - 6,
pos.getX(),
pos.getX() + 6 };
//The x coordinates of each vertex of the boat.
double[] x = {
pos.getX() - 6,
pos.getX(),
pos.getX() + 6};
//The y coordinates of each vertex of the boat.
double[] y = {
pos.getY() + 12,
pos.getY() - 12,
pos.getY() + 12 };
//The y coordinates of each vertex of the boat.
double[] y = {
pos.getY() + 12,
pos.getY() - 12,
pos.getY() + 12};
//The above shape is essentially a triangle 12px wide, and 24px long.
//The above shape is essentially a triangle 12px wide, and 24px long.
//Draw the boat.
gc.setFill(boat.getColor());
//Draw the boat.
gc.setFill(boat.getColor());
gc.save();
rotate(boat.getBearing().degrees(), pos.getX(), pos.getY());
gc.fillPolygon(x, y, 3);
gc.restore();
gc.save();
rotate(boat.getBearing().degrees(), pos.getX(), pos.getY());
gc.fillPolygon(x, y, 3);
gc.restore();
if (boat.getSourceID() == ThisBoat.getInstance().getSourceID()) {
drawSails(boat);
}
if (boat.getSourceID() == ThisBoat.getInstance().getSourceID()) {
drawSails(boat);
}
}
/**
* Draws extra decorations to show which boat has been assigned to the client.
* @param boat The client's boat.
*/
private void drawClientBoat(VisualiserBoat boat) {
//Convert position to graph coordinate.
GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
//The x coordinates of each vertex of the boat.
double[] x = {
pos.getX() - 9,
pos.getX(),
pos.getX() + 9 };
//The y coordinates of each vertex of the boat.
double[] y = {
pos.getY() + 15,
pos.getY() - 15,
pos.getY() + 15 };
//The above shape is essentially a triangle 24px wide, and 48 long.
//Draw the boat.
gc.setFill(Color.BLACK);
gc.save();
rotate(boat.getBearing().degrees(), pos.getX(), pos.getY());
gc.fillPolygon(x, y, 3);
gc.restore();
/**
* Draws sails for a given boat on the canvas. Sail position is
* determined by the boats heading and the current wind direction
@@ -324,7 +372,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
double xPos = boatPos.getX(); // x pos of sail (on boat)
double yPos = boatPos.getY() - 6; // y pos of sail (on boat)
double boatBearing = boat.getBearing().degrees();
double windDirection = visualiserRace.getWindDirection().degrees();
double windDirection = 0; //visualiserRace.getWindDirection().degrees();
double sailRotateAngle = 0; // rotation for correct sail display
Image sailImage = null;
Boolean rightSail = true;
@@ -393,7 +441,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);
}
}
@@ -441,7 +490,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()];
@@ -464,13 +513,20 @@ public class ResizableRaceCanvas extends ResizableCanvas {
* Called once per frame, and on canvas resize.
*/
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(); // clear the previous canvas
drawBoundary();
//Guiding Line
drawRaceLine();
if (annoGuideLine){
drawRaceLine();
}
//Boats.
drawBoats();
@@ -481,7 +537,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
* draws a transparent line around the course that shows the paths boats must travel
*/
public void drawRaceLine(){
List<Leg> legs = this.visualiserRace.getLegs();
List<Leg> legs = this.visualiserRace.getVisualiserRaceState().getLegs();
GPSCoordinate legStartPoint = legs.get(0).getStartCompoundMark().getAverageGPSCoordinate();
GPSCoordinate nextStartPoint;
for (int i = 0; i < legs.size() -1; i++) {
@@ -495,6 +551,7 @@ public class ResizableRaceCanvas extends ResizableCanvas {
* draws the line leg by leg
* @param legs the legs of a race
* @param index the index of the current leg to use
* @param legStartPoint The position the current leg.
* @return the end point of the current leg that has been drawn
*/
private GPSCoordinate drawLineRounding(List<Leg> legs, int index, GPSCoordinate legStartPoint){
@@ -627,7 +684,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());
@@ -1,13 +1,18 @@
package visualiser.model;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.paint.Color;
import shared.model.Leg;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@@ -23,7 +28,7 @@ public class Sparkline {
/**
* The race to observe.
*/
private VisualiserRace race;
private VisualiserRaceState race;
/**
* The boats to observe.
@@ -31,10 +36,10 @@ public class Sparkline {
private ObservableList<VisualiserBoat> boats;
/**
* The number of legs in the race.
* Used to correctly scale the linechart.
* Race legs to observe.
* We need to observe legs as they may be added after the sparkline is created if race.xml is received after this is created.
*/
private Integer legNum;
private ObservableList<Leg> legs;
/**
@@ -52,21 +57,31 @@ public class Sparkline {
*/
private NumberAxis yAxis;
/**
* A map between a boat and its data series in the sparkline.
* This is used so that we can remove a series when (or if) a boat is removed from the race.
*/
private Map<VisualiserBoat, XYChart.Series<Number, Number>> boatSeriesMap;
/**
* Constructor to set up initial sparkline (LineChart) object
* @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.legNum = race.getLegCount();
this.boats = new SortedList<>(race.getBoats());
this.legs = race.getLegs();
this.sparklineChart = sparklineChart;
this.yAxis = (NumberAxis) sparklineChart.getYAxis();
this.xAxis = (NumberAxis) sparklineChart.getXAxis();
this.boatSeriesMap = new HashMap<>();
createSparkline();
}
@@ -78,50 +93,45 @@ public class Sparkline {
* Position numbers are displayed.
*/
private void createSparkline() {
// NOTE: Y axis is in negatives to display correct positions
//For each boat...
for (VisualiserBoat boat : this.boats) {
//We need to dynamically update the sparkline when boats are added/removed.
boats.addListener((ListChangeListener.Change<? extends VisualiserBoat> c) -> {
//Create data series for each boat.
XYChart.Series<Number, Number> series = new XYChart.Series<>();
Platform.runLater(() -> {
while (c.next()) {
if (c.wasAdded()) {
for (VisualiserBoat boat : c.getAddedSubList()) {
addBoatSeries(boat);
}
} else if (c.wasRemoved()) {
for (VisualiserBoat boat : c.getRemoved()) {
removeBoatSeries(boat);
}
}
}
//Update height of y axis.
setYAxisLowerBound();
});
});
//All boats start in "last" place.
series.getData().add(new XYChart.Data<>(0, boats.size()));
//Listen for changes in the boat's leg - we only update the graph when it changes leg.
boat.legProperty().addListener(
(observable, oldValue, newValue) -> {
//Get the data to plot.
List<VisualiserBoat> boatOrder = race.getLegCompletionOrder().get(oldValue);
//Find boat position in list.
int boatPosition = boatOrder.indexOf(boat) + 1;
//Get leg number.
int legNumber = oldValue.getLegNumber() + 1;
//Create new data point for boat's position at the new leg.
XYChart.Data<Number, Number> dataPoint = new XYChart.Data<>(legNumber, boatPosition);
//Add to series.
Platform.runLater(() -> series.getData().add(dataPoint));
});
//Add to chart.
sparklineChart.getData().add(series);
//Color using boat's color. We need to do this after adding the series to a chart, otherwise we get null pointer exceptions.
series.getNode().setStyle("-fx-stroke: " + colourToHex(boat.getColor()) + ";");
legs.addListener((ListChangeListener.Change<? extends Leg> c) -> {
Platform.runLater(() -> xAxis.setUpperBound(race.getLegCount()));
});
//Initialise chart for existing boats.
for (VisualiserBoat boat : boats) {
addBoatSeries(boat);
}
sparklineChart.setCreateSymbols(false);
//Set x axis details
@@ -130,20 +140,109 @@ public class Sparkline {
xAxis.setTickLabelsVisible(false);
xAxis.setMinorTickVisible(false);
xAxis.setLowerBound(0);
xAxis.setUpperBound(legNum + 2);
xAxis.setUpperBound(race.getLegCount());
xAxis.setTickUnit(1);
//The y-axis uses negative values, with the minus sign hidden (e.g., boat in 1st has position -1, which becomes 1, boat in 6th has position -6, which becomes 6).
//This is necessary to actually get the y-axis labelled correctly. Negative tick count doesn't work.
//Set y axis details
yAxis.setLowerBound(boats.size());
yAxis.setUpperBound(1);
yAxis.setAutoRanging(false);
yAxis.setTickUnit(1);
yAxis.setMinorTickCount(0);
yAxis.setUpperBound(0);
setYAxisLowerBound();
yAxis.setLabel("Position in Race");
yAxis.setTickUnit(-1);//Negative tick reverses the y axis.
yAxis.setTickMarkVisible(true);
yAxis.setTickLabelsVisible(true);
yAxis.setTickMarkVisible(true);
yAxis.setMinorTickVisible(true);
yAxis.setMinorTickVisible(false);
//Hide minus number from displaying on axis.
yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis) {
@Override
public String toString(Number value) {
if ((value.intValue() == 0) || (value.intValue() < -boats.size())) {
return "";
} else {
return String.format("%d", -value.intValue());
}
}
});
}
/**
* Sets the lower bound of the y-axis.
*/
private void setYAxisLowerBound() {
yAxis.setLowerBound( -(boats.size() + 1));
}
/**
* Removes the data series for a given boat from the sparkline.
* @param boat Boat to remove series for.
*/
private void removeBoatSeries(VisualiserBoat boat) {
sparklineChart.getData().remove(boatSeriesMap.get(boat));
boatSeriesMap.remove(boat);
}
/**
* Creates a data series for a boat, and adds it to the sparkline.
* @param boat Boat to add series for.
*/
private void addBoatSeries(VisualiserBoat boat) {
//Create data series for boat.
XYChart.Series<Number, Number> series = new XYChart.Series<>();
//All boats start in "last" place.
series.getData().add(new XYChart.Data<>(0, -(boats.size())));
//Listen for changes in the boat's leg - we only update the graph when it changes leg.
boat.legProperty().addListener(
(observable, oldValue, newValue) -> {
//Get the data to plot.
List<VisualiserBoat> boatOrder = race.getLegCompletionOrder().get(oldValue);
//Find boat position in list.
int boatPosition = -(boatOrder.indexOf(boat) + 1);
//Get leg number.
int legNumber = oldValue.getLegNumber() + 1;
//Create new data point for boat's position at the new leg.
XYChart.Data<Number, Number> dataPoint = new XYChart.Data<>(legNumber, boatPosition);
//Add to series.
Platform.runLater(() -> series.getData().add(dataPoint));
});
//Add to chart.
sparklineChart.getData().add(series);
//Color using boat's color. We need to do this after adding the series to a chart, otherwise we get null pointer exceptions.
series.getNode().setStyle("-fx-stroke: " + colourToHex(boat.getColor()) + ";");
boatSeriesMap.put(boat, series);
}
@@ -55,6 +55,11 @@ public class VisualiserBoat extends Boat {
*/
private static final double wakeScale = 5;
/**
* If true then this boat has been allocated to the client.
*/
private boolean isClientBoat = false;
@@ -167,28 +172,33 @@ public class VisualiserBoat extends Boat {
*/
public String getTimeToNextMarkFormatted(ZonedDateTime currentTime) {
//Calculate time delta.
Duration timeUntil = Duration.between(currentTime, getEstimatedTimeAtNextMark());
if ((getTimeAtLastMark() != null) && (currentTime != 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 " -";
}
}
@@ -201,7 +211,7 @@ public class VisualiserBoat extends Boat {
*/
public String getTimeSinceLastMarkFormatted(ZonedDateTime currentTime) {
if (getTimeAtLastMark() != null) {
if ((getTimeAtLastMark() != null) && (currentTime != null)) {
//Calculate time delta.
Duration timeSince = Duration.between(getTimeAtLastMark(), currentTime);
@@ -212,4 +222,21 @@ public class VisualiserBoat extends Boat {
return " -";
}
}
/**
* Returns whether or not this boat has been assigned to the client.
* @return True if this is the client's boat.
*/
public boolean isClientBoat() {
return isClientBoat;
}
/**
* Sets whether or not this boat has been assigned to the client.
* @param clientBoat True if this is the client's boat.
*/
public void setClientBoat(boolean clientBoat) {
isClientBoat = clientBoat;
}
}
@@ -1,469 +0,0 @@
package visualiser.model;
import javafx.animation.AnimationTimer;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.paint.Color;
import network.Messages.BoatLocation;
import network.Messages.BoatStatus;
import network.Messages.Enums.BoatStatusEnum;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.LatestMessages;
import network.Messages.RaceStatus;
import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import shared.dataInput.RegattaDataSource;
import shared.model.*;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The Class used to view the race streamed.
* Has a course, boats, boundaries, etc...
* Observes LatestMessages and updates its state based on new messages.
*/
public class VisualiserRace extends Race implements Runnable {
/**
* An observable list of boats in the race.
*/
private final ObservableList<VisualiserBoat> boats;
/**
* An observable list of marker boats in the race.
*/
private ObservableList<Mark> boatMarkers;
/**
* Maps between a Leg to a list of boats, in the order that they finished the leg.
* Used by the Sparkline to ensure it has correct information.
*/
private Map<Leg, List<VisualiserBoat>> legCompletionOrder = new HashMap<>();
/**
* Constructs a race object with a given RaceDataSource, BoatDataSource, and RegattaDataSource and receives events from LatestMessages.
* @param boatDataSource Data source for boat related data (yachts and marker boats).
* @param raceDataSource Data source for race related data (participating boats, legs, etc...).
* @param regattaDataSource Data source for race related data (course name, location, timezone, etc...).
* @param latestMessages The LatestMessages to send events to.
* @param colors A collection of colors used to assign a color to each boat.
*/
public VisualiserRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, LatestMessages latestMessages, List<Color> colors) {
super(boatDataSource, raceDataSource, regattaDataSource, latestMessages);
this.boats = FXCollections.observableArrayList(this.generateVisualiserBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), colors));
this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values());
//Initialise the leg completion order map.
for (Leg leg : this.legs) {
this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size()));
}
}
/**
* Sets the race data source for this race to a new RaceDataSource.
* Uses the boundary and legs specified by the new RaceDataSource.
* @param raceDataSource The new RaceDataSource to use.
*/
public void setRaceDataSource(RaceDataSource raceDataSource) {
this.raceDataSource = raceDataSource;
this.boundary = raceDataSource.getBoundary();
this.useLegsList(raceDataSource.getLegs());
}
/**
* Sets the boat data source for this race to a new BoatDataSource.
* Uses the marker boats specified by the new BoatDataSource.
* @param boatDataSource The new BoatDataSource to use.
*/
public void setBoatDataSource(BoatDataSource boatDataSource) {
this.boatDataSource = boatDataSource;
this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values());
}
/**
* Sets the regatta data source for this race to a new RegattaDataSource.
* @param regattaDataSource The new RegattaDataSource to use.
*/
public void setRegattaDataSource(RegattaDataSource regattaDataSource) {
this.regattaDataSource = regattaDataSource;
}
/**
* Returns a list of {@link Mark} boats.
* @return List of mark boats.
*/
public ObservableList<Mark> getMarks() {
return boatMarkers;
}
/**
* Generates a list of VisualiserBoats given a list of Boats, and a list of participating boats.
* @param boats The map of Boats describing boats that are potentially in the race. Maps boat sourceID to boat.
* @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating.
* @param colors The list of colors to be used for the boats.
* @return A list of MockBoats that are participating in the race.
*/
private List<VisualiserBoat> generateVisualiserBoats(Map<Integer, Boat> boats, List<Integer> sourceIDs, List<Color> colors) {
List<VisualiserBoat> visualiserBoats = new ArrayList<>(sourceIDs.size());
//For each sourceID participating...
int colorIndex = 0;
for (int sourceID : sourceIDs) {
//Get the boat associated with the sourceID.
Boat boat = boats.get(sourceID);
//Get a color for the boat.
Color color = colors.get(colorIndex);
//Construct a VisualiserBoat using the Boat and Polars.
VisualiserBoat visualiserBoat = new VisualiserBoat(boat, color);
visualiserBoats.add(visualiserBoat);
//Next color.
colorIndex++;
}
return visualiserBoats;
}
/**
* Initialise the boats in the race.
* This sets their current leg.
*/
@Override
protected void initialiseBoats() {
Leg startingLeg = legs.get(0);
for (VisualiserBoat boat : boats) {
boat.setCurrentLeg(startingLeg);
boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
}
}
/**
* Updates all of the racing boats based on messages received.
* @param boats The list of racing boats.
* @param boatLocationMap A map between boat sourceIDs and BoatLocation messages.
* @param boatStatusMap A map between boat sourceIDs and BoatStatus messages.
*/
private void updateBoats(ObservableList<VisualiserBoat> boats, Map<Integer, BoatLocation> boatLocationMap, Map<Integer, BoatStatus> boatStatusMap) {
for (VisualiserBoat boat : boats) {
BoatLocation boatLocation = boatLocationMap.get(boat.getSourceID());
BoatStatus boatStatus = boatStatusMap.get(boat.getSourceID());
updateBoat(boat, boatLocation, boatStatus);
}
}
/**
* Updates an individual racing boat based on messages received.
* @param boat The boat to update.
* @param boatLocation The BoatLocation message to use.
* @param boatStatus The BoatStatus message to use.
*/
private void updateBoat(VisualiserBoat boat, BoatLocation boatLocation, BoatStatus boatStatus) {
if (boatLocation != null && boatStatus != null) {
//Get the new position.
double latitude = boatLocation.getLatitudeDouble();
double longitude = boatLocation.getLongitudeDouble();
GPSCoordinate gpsCoordinate = new GPSCoordinate(latitude, longitude);
boat.setCurrentPosition(gpsCoordinate);
//Bearing.
boat.setBearing(Bearing.fromDegrees(boatLocation.getHeadingDegrees()));
//Time until next mark.
boat.setEstimatedTimeAtNextMark(raceClock.getLocalTime(boatStatus.getEstTimeAtNextMark()));
//Speed.
boat.setCurrentSpeed(boatLocation.getBoatSOG() / Constants.KnotsToMMPerSecond);
//Boat status.
BoatStatusEnum newBoatStatusEnum = BoatStatusEnum.fromByte(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.getLatitudeDouble();
double longitude = boatLocation.getLongitudeDouble();
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 = RaceStatusEnum.fromByte(raceStatus.getRaceStatus());
//Wind.
this.setWind(
Bearing.fromDegrees(raceStatus.getScaledWindDirection()),
raceStatus.getWindSpeedKnots() );
//Current race time.
this.raceClock.setUTCTime(raceStatus.getCurrentTime());
}
/**
* Runnable for the thread.
*/
public void run() {
initialiseBoats();
startRaceStream();
}
/**
* Starts the race.
* This updates the race based on {@link #latestMessages}.
*/
private void startRaceStream() {
new AnimationTimer() {
long lastFrameTime = System.currentTimeMillis();
@Override
public void handle(long arg0) {
//Calculate the frame period.
long currentFrameTime = System.currentTimeMillis();
long framePeriod = currentFrameTime - lastFrameTime;
//Update race status.
updateRaceStatus(latestMessages.getRaceStatus());
//Update racing boats.
updateBoats(boats, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap());
//And their positions (e.g., 5th).
updateBoatPositions(boats);
//Update marker boats.
updateMarkers(boatMarkers, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap());
if (getRaceStatusEnum() == RaceStatusEnum.FINISHED) {
stop();
}
lastFrameTime = currentFrameTime;
//Increment fps.
incrementFps(framePeriod);
}
}.start();
}
/**
* Update position of boats in race (e.g, 5th), no position if on starting leg or DNF.
* @param boats The list of boats to update.
*/
private void updateBoatPositions(ObservableList<VisualiserBoat> boats) {
//Sort boats.
sortBoatsByPosition(boats);
//Assign new positions.
for (int i = 0; i < boats.size(); i++) {
VisualiserBoat boat = boats.get(i);
if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) {
boat.setPosition("-");
} else {
boat.setPosition(Integer.toString(i + 1));
}
}
}
/**
* Sorts the list of boats by their position within the race.
* @param boats The list of boats in the race.
*/
private void sortBoatsByPosition(ObservableList<VisualiserBoat> boats) {
FXCollections.sort(boats, (a, b) -> {
//Get the difference in leg numbers.
int legNumberDelta = b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber();
//If they're on the same leg, we need to compare time to finish leg.
if (legNumberDelta == 0) {
return (int) Duration.between(b.getEstimatedTimeAtNextMark(), a.getEstimatedTimeAtNextMark()).toMillis();
} else {
return legNumberDelta;
}
});
}
/**
* Returns the boats participating in the race.
* @return ObservableList of boats participating in the race.
*/
public ObservableList<VisualiserBoat> getBoats() {
return boats;
}
/**
* Returns the order in which boats completed each leg. Maps the leg to a list of boats, ordered by the order in which they finished the leg.
* @return Leg completion order for each leg.
*/
public Map<Leg, List<VisualiserBoat>> getLegCompletionOrder() {
return legCompletionOrder;
}
/**
* Takes an estimated time an event will occur, and converts it to the
* number of seconds before the event will occur.
*
* @param estTimeMillis The estimated time, in milliseconds.
* @param currentTime The current time, in milliseconds.
* @return int difference between time the race started and the estimated time
*/
private int convertEstTime(long estTimeMillis, long currentTime) {
//Calculate millisecond delta.
long estElapsedMillis = estTimeMillis - currentTime;
//Convert milliseconds to seconds.
int estElapsedSecs = Math.round(estElapsedMillis / 1000);
return estElapsedSecs;
}
}
@@ -0,0 +1,79 @@
package visualiser.model;
import mock.exceptions.CommandConstructionException;
import mock.model.commandFactory.Command;
import mock.model.commandFactory.CompositeCommand;
import network.Messages.*;
import shared.model.RunnableWithFramePeriod;
import visualiser.Commands.VisualiserRaceCommands.VisualiserRaceCommandFactory;
import java.util.concurrent.BlockingQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* The controller for race related messages, coming from the server to the client.
*/
public class VisualiserRaceController implements RunnableWithFramePeriod {
/**
* Incoming messages from server.
*/
private BlockingQueue<AC35Data> incomingMessages;
/**
* Commands are placed in here, and executed by visualiserRace.
*/
private CompositeCommand compositeRaceCommand;
/**
* The context that created commands operate on.
*/
private VisualiserRaceState visualiserRace;
/**
* Constructs a visualiserInput to convert an incoming stream of messages into commands.
* @param incomingMessages The incoming queue of messages.
* @param visualiserRace The context to for commands to operate on.
* @param compositeRaceCommand The composite command to place command in.
*/
public VisualiserRaceController(BlockingQueue<AC35Data> incomingMessages, VisualiserRaceState visualiserRace, CompositeCommand compositeRaceCommand) {
this.incomingMessages = incomingMessages;
this.compositeRaceCommand = compositeRaceCommand;
this.visualiserRace = visualiserRace;
}
@Override
public void run() {
while (!Thread.interrupted()) {
try {
AC35Data message = incomingMessages.take();
Command command = VisualiserRaceCommandFactory.create(message, visualiserRace);
compositeRaceCommand.addCommand(command);
} catch (CommandConstructionException e) {
Logger.getGlobal().log(Level.WARNING, "VisualiserRaceController could not create a command for incoming message.");
} catch (InterruptedException e) {
Logger.getGlobal().log(Level.SEVERE, "VisualiserRaceController was interrupted on thread: " + Thread.currentThread() + " while waiting for messages.");
Thread.currentThread().interrupt();
return;
}
}
}
}
@@ -0,0 +1,119 @@
package visualiser.model;
import javafx.beans.property.IntegerProperty;
import mock.model.commandFactory.CompositeCommand;
import network.Messages.Enums.RequestToJoinEnum;
import shared.dataInput.EmptyBoatDataSource;
import shared.dataInput.EmptyRaceDataSource;
import shared.dataInput.EmptyRegattaDataSource;
import visualiser.gameController.ControllerClient;
import visualiser.network.ServerConnection;
import java.io.IOException;
import java.net.Socket;
/**
* This class holds a race, and a client's connection to it
*/
public class VisualiserRaceEvent {
/**
* Our connection to the server.
*/
private ServerConnection serverConnection;
/**
* The thread serverConnection is running on.
*/
private Thread serverConnectionThread;
/**
* The race object which describes the currently occurring race.
*/
private VisualiserRaceState visualiserRaceState;
/**
* The service for updating the {@link #visualiserRaceState}.
*/
private VisualiserRaceService visualiserRaceService;
/**
* The thread {@link #visualiserRaceService} is running on.
*/
private Thread visualiserRaceServiceThread;
/**
* Creates a visualiser race event, with a given socket and request type.
* @param socket The socket to connect to.
* @param requestType The type of {@link network.Messages.RequestToJoin} to make.
* @throws IOException Thrown if there is a problem with the socket.
*/
public VisualiserRaceEvent(Socket socket, RequestToJoinEnum requestType) throws IOException {
this.visualiserRaceState = new VisualiserRaceState(new EmptyRaceDataSource(), new EmptyRegattaDataSource(), new EmptyBoatDataSource());
CompositeCommand raceCommands = new CompositeCommand();
this.visualiserRaceService = new VisualiserRaceService(raceCommands, visualiserRaceState);
this.visualiserRaceServiceThread = new Thread(visualiserRaceService, "VisualiserRaceEvent()->VisualiserRaceService thread " + visualiserRaceService);
this.visualiserRaceServiceThread.start();
this.serverConnection = new ServerConnection(socket, visualiserRaceState, raceCommands, requestType);
this.serverConnectionThread = new Thread(serverConnection, "StartController.enterLobby()->serverConnection thread " + serverConnection);
this.serverConnectionThread.start();
}
/**
* Returns the state of the race.
* @return The state of the race.
*/
public VisualiserRaceState getVisualiserRaceState() {
return visualiserRaceState;
}
/**
* Returns the controller client, which writes BoatAction messages to the outgoing queue.
* @return The ControllerClient.
*/
public ControllerClient getControllerClient() {
return serverConnection.getControllerClient();
}
/**
* Returns the connection to server.
* @return Connection to server.
*/
public ServerConnection getServerConnection() {
return serverConnection;
}
/**
* Returns the framerate property of the race.
* @return Framerate property of race.
*/
public IntegerProperty getFrameRateProperty() {
return visualiserRaceService.getFrameRateProperty();
}
/**
* Terminates the server connection and race service.
*/
public void terminate() {
this.serverConnectionThread.interrupt();
this.visualiserRaceServiceThread.interrupt();
}
}
@@ -0,0 +1,93 @@
package visualiser.model;
import javafx.beans.property.IntegerProperty;
import mock.model.commandFactory.CompositeCommand;
import shared.model.FrameRateTracker;
import shared.model.RunnableWithFramePeriod;
/**
* Handles updating a {@link VisualiserRaceState} with incoming commands.
*/
public class VisualiserRaceService implements RunnableWithFramePeriod {
/**
* The race state to update.
*/
private VisualiserRaceState visualiserRaceState;
/**
* A composite commands to execute to update the race.
*/
private CompositeCommand raceCommands;
/**
* Used to track the framerate of the "simulation".
*/
private FrameRateTracker frameRateTracker;
/**
* Constructs a visualiser race which models a yacht race, and is modified by CompositeCommand.
* @param raceCommands A composite commands to execute to update the race.
* @param visualiserRaceState The race state to update.
*/
public VisualiserRaceService(CompositeCommand raceCommands, VisualiserRaceState visualiserRaceState) {
this.raceCommands = raceCommands;
this.visualiserRaceState = visualiserRaceState;
this.frameRateTracker = new FrameRateTracker();
}
/**
* Returns the CompositeCommand executed by the race.
* @return CompositeCommand executed by race.
*/
public CompositeCommand getRaceCommands() {
return raceCommands;
}
@Override
public void run() {
long previousFrameTime = System.currentTimeMillis();
while (!Thread.interrupted()) {
long currentFrameTime = System.currentTimeMillis();
waitForFramePeriod(previousFrameTime, currentFrameTime, 16);
previousFrameTime = currentFrameTime;
raceCommands.execute();
}
frameRateTracker.stop();
}
/**
* Returns the framerate property of the race.
* @return Framerate property of race.
*/
public IntegerProperty getFrameRateProperty() {
return frameRateTracker.fpsProperty();
}
}
@@ -0,0 +1,408 @@
package visualiser.model;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.paint.Color;
import network.Messages.Enums.BoatStatusEnum;
import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import shared.dataInput.RegattaDataSource;
import shared.exceptions.BoatNotFoundException;
import shared.exceptions.MarkNotFoundException;
import shared.model.*;
import java.time.Duration;
import java.util.*;
/**
* This class contains all of the state of a race on the client (visualiser) side.
*/
public class VisualiserRaceState extends RaceState {
/**
* A list of boats in the race.
*/
private ObservableList<VisualiserBoat> boats;
/**
* The source ID of the boat assigned to the player.
* 0 if no boat has been assigned.
*/
private int playerBoatID;
/**
* Maps between a Leg to a list of boats, in the order that they finished the leg.
* Used by the Sparkline to ensure it has correct information.
* TODO BUG: if we receive a race.xml file during the race, then we need to add/remove legs to this, without losing information.
*/
private Map<Leg, List<VisualiserBoat>> legCompletionOrder;
/**
* An array of colors used to assign colors to each boat - passed in to the VisualiserRace constructor.
*/
private List<Color> unassignedColors = new ArrayList<>(Arrays.asList(
Color.BLUEVIOLET,
Color.BLACK,
Color.RED,
Color.ORANGE,
Color.DARKOLIVEGREEN,
Color.LIMEGREEN,
Color.PURPLE,
Color.DARKGRAY,
Color.YELLOW
//TODO may need to add more colors.
));
/**
* Constructs a visualiser race which models a yacht race.
* @param raceDataSource The raceDataSource to initialise with.
* @param regattaDataSource The regattaDataSource to initialise with.
* @param boatDataSource The boatDataSource to initialise with.
*/
public VisualiserRaceState(RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, BoatDataSource boatDataSource) {
this.boats = FXCollections.observableArrayList();
this.playerBoatID = 0;
this.legCompletionOrder = new HashMap<>();
setRaceDataSource(raceDataSource);
setRegattaDataSource(regattaDataSource);
setBoatDataSource(boatDataSource);
}
/**
* Sets the race data source for this race to a new RaceDataSource.
* Uses the boundary and legs specified by the new RaceDataSource.
* @param raceDataSource The new RaceDataSource to use.
*/
public void setRaceDataSource(RaceDataSource raceDataSource) {
super.setRaceDataSource(raceDataSource);
if (getBoatDataSource() != null) {
this.generateVisualiserBoats(this.boats, getBoatDataSource().getBoats(), raceDataSource.getParticipants(), unassignedColors);
}
initialiseLegCompletionOrder();
}
/**
* Sets the boat data source for this race to a new BoatDataSource.
* Uses the marker boats specified by the new BoatDataSource.
* @param boatDataSource The new BoatDataSource to use.
*/
public void setBoatDataSource(BoatDataSource boatDataSource) {
super.setBoatDataSource(boatDataSource);
if (getRaceDataSource() != null) {
this.generateVisualiserBoats(this.boats, boatDataSource.getBoats(), getRaceDataSource().getParticipants(), unassignedColors);
}
}
/**
* Sets the regatta data source for this race to a new RegattaDataSource.
* @param regattaDataSource The new RegattaDataSource to use.
*/
public void setRegattaDataSource(RegattaDataSource regattaDataSource) {
super.setRegattaDataSource(regattaDataSource);
}
/**
* Initialises the {@link #legCompletionOrder} map.
*/
public void initialiseLegCompletionOrder() {
//Initialise the leg completion order map.
for (Leg leg : getLegs()) {
this.legCompletionOrder.put(leg, new ArrayList<>(this.boats.size()));
}
}
/**
* Generates a list of VisualiserBoats given a list of Boats, and a list of participating boats.
* This will add VisualiserBoats for newly participating sourceID, and remove VisualiserBoats for any participating sourceIDs that have been removed.
*
* @param existingBoats The visualiser boats that already exist in the race. This will be populated when we receive a new race.xml or boats.xml.
* @param boats The map of {@link Boat}s describing boats that are potentially in the race. Maps boat sourceID to boat.
* @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating.
* @param colors The list of unassignedColors to be used for the boats.
*/
private void generateVisualiserBoats(ObservableList<VisualiserBoat> existingBoats, Map<Integer, Boat> boats, List<Integer> sourceIDs, List<Color> colors) {
//Remove any VisualiserBoats that are no longer participating.
for (VisualiserBoat boat : new ArrayList<>(existingBoats)) {
//Boat no longer is participating.
if (!sourceIDs.contains(boat.getSourceID())) {
//Return their colors to the color list.
colors.add(boat.getColor());
//Remove boat.
existingBoats.remove(boat);
}
}
//Get source IDs of already existing boats.
List<Integer> existingBoatIDs = new ArrayList<>();
for (VisualiserBoat boat : existingBoats) {
existingBoatIDs.add(boat.getSourceID());
}
//Get source IDs of only newly participating boats.
List<Integer> newBoatIDs = new ArrayList<>(sourceIDs);
newBoatIDs.removeAll(existingBoatIDs);
//Create VisualiserBoat for newly participating boats.
for (Integer sourceID : newBoatIDs) {
if (boats.containsKey(sourceID)) {
VisualiserBoat boat = new VisualiserBoat(
boats.get(sourceID),
colors.remove(colors.size() - 1));//TODO potential bug: not enough colors for boats.
boat.setCurrentLeg(getLegs().get(0));
existingBoats.add(boat);
}
}
setPlayerBoat();
}
/**
* Sets the boat the player has been assigned to as belonging to them.
*/
private void setPlayerBoat() {
if (getPlayerBoatID() != 0) {
for (VisualiserBoat boat : new ArrayList<>(getBoats())) {
if (boat.getSourceID() == getPlayerBoatID()) {
boat.setClientBoat(true);
}
}
}
}
/**
* Initialise the boats in the race.
* This sets their current leg.
*/
@Override
protected void initialiseBoats() {
Leg startingLeg = getLegs().get(0);
for (VisualiserBoat boat : boats) {
boat.setCurrentLeg(startingLeg);
boat.setTimeAtLastMark(getRaceClock().getCurrentTime());
boat.setCurrentPosition(new GPSCoordinate(0, 0));
}
}
/**
* Update position of boats in race (e.g, 5th), no position if on starting leg or DNF.
* @param boats The list of boats to update.
*/
public void updateBoatPositions(List<VisualiserBoat> boats) {
//Sort boats.
sortBoatsByPosition(boats);
//Assign new positions.
for (int i = 0; i < boats.size(); i++) {
VisualiserBoat boat = boats.get(i);
if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) {
boat.setPosition("-");
} else {
boat.setPosition(Integer.toString(i + 1));
}
}
}
/**
* Sorts the list of boats by their position within the race.
* @param boats The list of boats in the race.
*/
private void sortBoatsByPosition(List<VisualiserBoat> boats) {
boats.sort((a, b) -> {
//Get the difference in leg numbers.
int legNumberDelta = b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber();
//If they're on the same leg, we need to compare time to finish leg.
if (legNumberDelta == 0) {
//These are potentially null until we receive our first RaceStatus containing BoatStatuses.
if ((a.getEstimatedTimeAtNextMark() != null) && (b.getEstimatedTimeAtNextMark() != null)) {
return (int) Duration.between(
b.getEstimatedTimeAtNextMark(),
a.getEstimatedTimeAtNextMark() ).toMillis();
}
}
return legNumberDelta;
});
}
/**
* Returns the boats participating in the race.
* @return List of boats participating in the race.
*/
public ObservableList<VisualiserBoat> getBoats() {
return boats;
}
/**
* Returns a boat by sourceID.
* @param sourceID The source ID the boat.
* @return The boat.
* @throws BoatNotFoundException Thrown if there is no boat with the specified sourceID.
*/
public VisualiserBoat getBoat(int sourceID) throws BoatNotFoundException {
for (VisualiserBoat boat : boats) {
if (boat.getSourceID() == sourceID) {
return boat;
}
}
throw new BoatNotFoundException("Boat with sourceID: " + sourceID + " was not found.");
}
/**
* Returns whether or not there exists a {@link VisualiserBoat} with the given source ID.
* @param sourceID SourceID of VisualiserBoat.
* @return True if VisualiserBoat exists, false otherwise.
*/
public boolean isVisualiserBoat(int sourceID) {
try {
getBoat(sourceID);
return true;
} catch (BoatNotFoundException e) {
return false;
}
}
/**
* Returns a mark by sourceID.
* @param sourceID The source ID the mark.
* @return The mark.
* @throws MarkNotFoundException Thrown if there is no mark with the specified sourceID.
*/
public Mark getMark(int sourceID) throws MarkNotFoundException {
for (Mark mark : getMarks()) {
if (mark.getSourceID() == sourceID) {
return mark;
}
}
throw new MarkNotFoundException("Mark with sourceID: " + sourceID + " was not found.");
}
/**
* Returns whether or not there exists a {@link Mark} with the given source ID.
* @param sourceID SourceID of mark.
* @return True if mark exists, false otherwise.
*/
public boolean isMark(int sourceID) {
try {
getMark(sourceID);
return true;
} catch (MarkNotFoundException e) {
return false;
}
}
/**
* Returns the order in which boats completed each leg. Maps the leg to a list of boats, ordered by the order in which they finished the leg.
* @return Leg completion order for each leg.
*/
public Map<Leg, List<VisualiserBoat>> getLegCompletionOrder() {
return legCompletionOrder;
}
/**
* Gets the source ID of the player's boat. 0 if not assigned.
* @return Players boat source ID.
*/
public int getPlayerBoatID() {
return playerBoatID;
}
/**
* sets the source ID of the player's boat. 0 if not assigned.
* @param playerBoatID Players boat source ID.
*/
public void setPlayerBoatID(int playerBoatID) {
this.playerBoatID = playerBoatID;
setPlayerBoat();
}
}