I had accidentally broken the raceXML construction - fixed it.

Fixed a few issues where boat didn't have nextMark/previousMark times under certain circumstances.
LatestMessages wasn't actually notifying properly - fixed.
The various XML readers now treat a string constructor as containing file contents, not file name - this was how it was mostly already used.
Fixed some issues in RaceXMLReader, including where element.getChildNodes() was used instead of element.getElementsByName(...).
Boat: changes speed to a doubleProperty. Changed leg to a Property<Leg>.
Race.lastFps is now an IntegerProperty.
Added breaks to the case statements in VisualiserInput. Whoops.
ResizableRaceCanvas now handles drawing the race boundary. Tidied up code a bit.
Removed ResizableRaceMap.

Started refactoring Sparkline. Currently doesn't work.
main
fjc40 9 years ago
parent abbbf70146
commit 7d3cf6ee80

@ -72,6 +72,7 @@ public class Event {
} catch (XMLReaderException | InvalidBoatDataException | InvalidRaceDataException | InvalidRegattaDataException e) {
e.printStackTrace();
//TODO if this occurs, we should print and error and abort.
}
}
@ -89,7 +90,7 @@ public class Event {
/**
* Sets the xml description of the race to show the race was created now, and starts in 4 minutes
* @param raceXML
* @param raceXML The race.xml contents.
* @return String containing edited xml
*/
private String getRaceXMLAtCurrentTime(String raceXML) {
@ -102,9 +103,9 @@ public class Event {
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
ZonedDateTime creationTime = ZonedDateTime.now();
raceXML.replace("CREATION_TIME", dateFormat.format(creationTime));
raceXML = raceXML.replace("CREATION_TIME", dateFormat.format(creationTime));
raceXML.replace("START_TIME", dateFormat.format(creationTime.plusSeconds(secondsToAdd)));
raceXML = raceXML.replace("START_TIME", dateFormat.format(creationTime.plusSeconds(secondsToAdd)));
return raceXML;

@ -1,7 +1,6 @@
package mock.model;
import javafx.animation.AnimationTimer;
import mock.app.MockOutput;
import network.Messages.BoatLocation;
import network.Messages.BoatStatus;
import network.Messages.Enums.BoatStatusEnum;
@ -14,6 +13,9 @@ import network.Messages.Enums.RaceStatusEnum;
import shared.dataInput.RegattaDataSource;
import shared.model.*;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.*;
import static java.lang.Math.cos;
@ -239,8 +241,8 @@ public class MockRace extends Race {
*/
private void updateRaceStatusEnum() {
//The amount of milliseconds until the race starts.
long timeToStart = this.raceClock.getDurationMilli();
//The millisecond duration of the race. Negative means it hasn't started, so we flip sign.
long timeToStart = - this.raceClock.getDurationMilli();
if (timeToStart > Constants.RacePreStartTime) {
@ -279,7 +281,7 @@ public class MockRace extends Race {
boat.getSourceID(),
boat.getStatus(),
boat.getCurrentLeg().getLegNumber(),
boat.getEstimatedTime() );
boat.getEstimatedTimeAtNextMark().toInstant().toEpochMilli() );
boatStatuses.add(boatStatus);
}
@ -318,6 +320,18 @@ public class MockRace extends Race {
}
/**
* Sets the estimated time at next mark for each boat to a specified time. This is used during the countdown timer to provide this value to boat before the race starts.
* @param time The time to provide to each boat.
*/
private void setBoatsTimeNextMark(ZonedDateTime time) {
for (MockBoat boat : this.boats) {
boat.setEstimatedTimeAtNextMark(time);
}
}
/**
* Countdown timer until race starts.
*/
@ -335,6 +349,9 @@ public class MockRace extends Race {
//Update the race status based on the current time.
updateRaceStatusEnum();
//Provide boat's with an estimated time at next mark until the race starts.
setBoatsTimeNextMark(raceClock.getCurrentTime());
//Parse the boat locations.
parseBoatLocations();
@ -910,8 +927,13 @@ public class MockRace extends Race {
double velocityToMark = boat.getCurrentSpeed() * cos(boat.getBearing().radians() - boat.calculateBearingToNextMarker().radians()) / Constants.KnotsToMMPerSecond;
if (velocityToMark > 0) {
//Calculate milliseconds until boat reaches mark.
long timeFromNow = (long) (1000 * boat.calculateDistanceToNextMarker() / velocityToMark);
boat.setEstimatedTime(this.raceClock.getCurrentTimeMilli() + timeFromNow);
//Calculate time at which it will reach mark.
ZonedDateTime timeAtMark = this.raceClock.getCurrentTime().plus(timeFromNow, ChronoUnit.MILLIS);
boat.setEstimatedTimeAtNextMark(timeAtMark);
}
}

@ -140,7 +140,7 @@ public class LatestMessages extends Observable {
* @param markRounding The MarkRounding message to set.
*/
public void setMarkRounding(MarkRounding markRounding) {
//TODO should compare the sequence number of the new boatLocation with the existing boatLocation for this boat (if it exists), and use the newer one.
//TODO should compare the sequence number of the new markRounding with the existing boatLocation for this boat (if it exists), and use the newer one.
markRoundingMap.put(markRounding.getSourceID(), markRounding);
}
@ -221,6 +221,7 @@ public class LatestMessages extends Observable {
public void setRaceXMLMessage(XMLMessage raceXMLMessage) {
this.raceXMLMessage = raceXMLMessage;
this.setChanged();
this.notifyObservers();
}
@ -240,6 +241,7 @@ public class LatestMessages extends Observable {
public void setBoatXMLMessage(XMLMessage boatXMLMessage) {
this.boatXMLMessage = boatXMLMessage;
this.setChanged();
this.notifyObservers();
}
@ -259,6 +261,7 @@ public class LatestMessages extends Observable {
public void setRegattaXMLMessage(XMLMessage regattaXMLMessage) {
this.regattaXMLMessage = regattaXMLMessage;
this.setChanged();
this.notifyObservers();
}

@ -31,12 +31,12 @@ public class BoatXMLReader extends XMLReader implements BoatDataSource {
/**
* Constructor for Boat XML using a file read as a resource.
*
* @param filePath Name/path of file to read. Read as a resource.
* @param fileContents Contents of xml file.
* @throws XMLReaderException Thrown if the file cannot be parsed.
* @throws InvalidBoatDataException Thrown if the file cannot be parsed correctly.
*/
public BoatXMLReader(String filePath) throws XMLReaderException, InvalidBoatDataException {
super(filePath);
public BoatXMLReader(String fileContents) throws XMLReaderException, InvalidBoatDataException {
super(fileContents);
//Attempt to read boat xml file.
try {

@ -82,13 +82,13 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
/**
* Constructor for Streamed Race XML
* @param filePath file path to read
* @param fileContents Contents of xml file.
* @throws XMLReaderException Thrown if an XML reader cannot be constructed for the given file.
* @throws InvalidRaceDataException Thrown if the XML file is invalid in some way.
*/
public RaceXMLReader(String filePath) throws XMLReaderException, InvalidRaceDataException {
public RaceXMLReader(String fileContents) throws XMLReaderException, InvalidRaceDataException {
super(filePath);
super(fileContents);
//Attempt to read race xml file.
@ -101,8 +101,9 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
/**
* Reads the contents of the race xml file.
* @throws InvalidRaceDataException Thrown if we cannot parse the document properly.
*/
private void read() {
private void read() throws InvalidRaceDataException {
readRace();
readParticipants();
readCourse();
@ -127,7 +128,7 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
//Race type.
String raceTypeString = getTextValueOfNode(settings, "RaceType");
raceType = RaceTypeEnum.valueOf(raceTypeString);
raceType = RaceTypeEnum.fromString(raceTypeString);
//XML creation time.
creationTimeDate = ZonedDateTime.parse(getTextValueOfNode(settings, "CreationTimeDate"), dateFormat);
@ -178,8 +179,9 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
/**
* Reads course data from the xml file.
* @throws InvalidRaceDataException Thrown if we cannot parse the document properly.
*/
private void readCourse() {
private void readCourse() throws InvalidRaceDataException {
readCompoundMarks();
readCompoundMarkSequence();
readCourseLimits();
@ -189,30 +191,31 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
/**
* Indexes CompoundMark elements by their ID for use in generating the course, and populates list of Markers.
* @throws InvalidRaceDataException thrown if we cannot create a compound mark from the document.
* @see CompoundMark
*/
private void readCompoundMarks() {
private void readCompoundMarks() throws InvalidRaceDataException {
//Gets the "<Course>...</..>" element.
Element course = (Element) doc.getElementsByTagName("Course").item(0);
//Get the list of CompoundMark elements.
NodeList compoundMarkList = course.getElementsByTagName("CompoundMark");
//Number of compound marks in the course.
int numberOfCompoundMarks = course.getChildNodes().getLength();
int numberOfCompoundMarks = compoundMarkList.getLength();
//For each CompoundMark element, create a CompoundMark object.
for(int i = 0; i < numberOfCompoundMarks; i++) {
//Get the CompoundMark element.
Element compoundMarkElement = (Element) course.getChildNodes().item(i);
//If it is actually a CompoundMark element, turn it into a CompoundMark object.
if(compoundMarkElement.getNodeName().equals("CompoundMark")) {
Element compoundMarkElement = (Element) compoundMarkList.item(i);
CompoundMark compoundMark = createCompoundMark(compoundMarkElement);
compoundMarkMap.put(compoundMark.getId(), compoundMark);
//Convert to CompoundMark object.
CompoundMark compoundMark = createCompoundMark(compoundMarkElement);
}
compoundMarkMap.put(compoundMark.getId(), compoundMark);
}
}
@ -321,7 +324,7 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
NodeList corners = compoundMarkSequence.getElementsByTagName("Corner");
//Gets the first corner.
Element cornerElement = (Element)corners.item(0);
Element cornerElement = (Element) corners.item(0);
//Gets the ID number of this corner element.
int cornerID = getCompoundMarkID(cornerElement);
@ -365,20 +368,19 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
//The "<CourseLimit>...</...>" element. This contains a sequence of Limit elements.
Element courseLimit = (Element) doc.getElementsByTagName("CourseLimit").item(0);
//Get the list of Limit elements.
NodeList limitList = courseLimit.getElementsByTagName("Limit");
//For each limit element...
for(int i = 0; i < courseLimit.getChildNodes().getLength(); i++) {
for(int i = 0; i < limitList.getLength(); i++) {
//Get the Limit element.
Element limit = (Element) courseLimit.getChildNodes().item(i);
Element limit = (Element) limitList.item(i);
//If it is actually a Limit element, add the limit to boundary list.
if (limit.getNodeName().equals("Limit")) {
double latitude = Double.parseDouble(limit.getAttribute("Lat"));
double longitude = Double.parseDouble(limit.getAttribute("Lon"));
boundary.add(new GPSCoordinate(latitude, longitude));
}
//Convert to GPSCoordinate.
double latitude = Double.parseDouble(limit.getAttribute("Lat"));
double longitude = Double.parseDouble(limit.getAttribute("Lon"));
boundary.add(new GPSCoordinate(latitude, longitude));
}
}

@ -66,12 +66,12 @@ public class RegattaXMLReader extends XMLReader implements RegattaDataSource {
/**
* Constructor for Regatta XML using a file read as a resource.
*
* @param filePath path of the file to read. Read as a resource.
* @param fileContents Contents of xml file.
* @throws XMLReaderException Thrown if the file cannot be parsed.
* @throws InvalidRegattaDataException Thrown if the file cannot be parsed correctly.
*/
public RegattaXMLReader(String filePath) throws XMLReaderException, InvalidRegattaDataException {
super(filePath);
public RegattaXMLReader(String fileContents) throws XMLReaderException, InvalidRegattaDataException {
super(fileContents);
//Attempt to read boat xml file.
try {

@ -15,9 +15,8 @@ import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.*;
import java.nio.charset.StandardCharsets;
/**
* Base Reader for XML Files
@ -27,14 +26,14 @@ public abstract class XMLReader {
protected Document doc;
/**
* Read an XML file by name as a resource.
* @param filePath filepath for XML file. Loaded as a resource.
* Read an XML file.
* @param fileContents Contents of the xml file.
* @throws XMLReaderException Thrown if the file cannot be parsed.
*/
public XMLReader(String filePath) throws XMLReaderException {
public XMLReader(String fileContents) throws XMLReaderException {
//Read file as resource.
InputStream xmlInputStream = getClass().getClassLoader().getResourceAsStream(filePath);
//Wrap file contents in input stream.
InputStream xmlInputStream = new ByteArrayInputStream(fileContents.getBytes(StandardCharsets.UTF_8));
this.doc = parseInputStream(xmlInputStream);

@ -1,10 +1,12 @@
package shared.model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import com.sun.istack.internal.Nullable;
import javafx.beans.property.*;
import network.Messages.Enums.BoatStatusEnum;
import java.time.ZonedDateTime;
/**
* Boat Model that is used to store information on the boats that are running in the race.
*/
@ -18,7 +20,7 @@ public class Boat {
* The current speed of the boat, in knots.
* TODO knots
*/
private double currentSpeed;
private DoubleProperty currentSpeed = new SimpleDoubleProperty(0);
/**
* The current bearing/heading of the boat.
@ -44,7 +46,7 @@ public class Boat {
/**
* The leg of the race that the boat is currently on.
*/
private Leg currentLeg;
private Property<Leg> currentLeg = new SimpleObjectProperty<>();
/**
* The distance, in meters, that the boat has travelled in the current leg.
@ -55,7 +57,7 @@ public class Boat {
/**
* The boat's position within the race (e.g., 5th).
*/
private StringProperty positionInRace;
private StringProperty positionInRace = new SimpleStringProperty();
/**
* The time, in milliseconds, that has elapsed during the current leg.
@ -77,9 +79,15 @@ public class Boat {
/**
* The amount of time, in seconds, until the boat reaches the next mark.
* The time at which the boat is estimated to reach the next mark, in milliseconds since unix epoch.
*/
private ZonedDateTime estimatedTimeAtNextMark;
/**
* The time at which the boat reached the previous mark.
*/
private long estimatedTime = 0;
@Nullable
private ZonedDateTime timeAtLastMark;
/**
@ -132,7 +140,7 @@ public class Boat {
* @return The current speed of the boat, in knots.
*/
public double getCurrentSpeed() {
return currentSpeed;
return currentSpeed.get();
}
/**
@ -140,7 +148,15 @@ public class Boat {
* @param currentSpeed The new speed of the boat, in knots.
*/
public void setCurrentSpeed(double currentSpeed) {
this.currentSpeed = currentSpeed;
this.currentSpeed.set(currentSpeed);
}
/**
* Returns the current speed property of the boat.
* @return The current speed of the boat, in a DoubleProperty.
*/
public DoubleProperty currentSpeedProperty() {
return currentSpeed;
}
@ -189,6 +205,14 @@ public class Boat {
* @return The current leg of the race the boat is in.
*/
public Leg getCurrentLeg() {
return currentLeg.getValue();
}
/**
* Returns the current leg, wrapped in a property.
* @return Current leg, wrapped in a property.
*/
public Property<Leg> legProperty() {
return currentLeg;
}
@ -198,7 +222,7 @@ public class Boat {
* @param currentLeg The new leg of the race the boat is in.
*/
public void setCurrentLeg(Leg currentLeg) {
this.currentLeg = currentLeg;
this.currentLeg.setValue(currentLeg);
this.setTimeElapsedInCurrentLeg(0);
this.setDistanceTravelledInLeg(0);
}
@ -221,14 +245,26 @@ public class Boat {
}
/**
* Returns the position within the race the boat has (e.g., 5th).
* @return The boat's position in race.
*/
public StringProperty positionProperty() {
return positionInRace;
}
/**
* Sets the position within the race the boat has (e.g., 5th).
* @param position The boat's position in race.
*/
public void setPosition(String position) {
this.positionInRace.set(position);
}
/**
* Returns the position within the race the boat has (e.g., 5th).
* @return The boat's position in race.
*/
public String getPosition() {
return this.positionInRace.get();
}
@ -321,13 +357,38 @@ public class Boat {
}
/**
* Returns the time at which the boat should reach the next mark.
* @return Time at which the boat should reach next mark.
*/
public ZonedDateTime getEstimatedTimeAtNextMark() {
return estimatedTimeAtNextMark;
}
/**
* Sets the time at which the boat should reach the next mark.
* @param estimatedTimeAtNextMark Time at which the boat should reach next mark.
*/
public void setEstimatedTimeAtNextMark(ZonedDateTime estimatedTimeAtNextMark) {
this.estimatedTimeAtNextMark = estimatedTimeAtNextMark;
}
public long getEstimatedTime() {
return estimatedTime;
/**
* Returns the time at which the boat reached the previous mark.
* @return The time at which the boat reached the previous mark. May be null.
*/
@Nullable
public ZonedDateTime getTimeAtLastMark() {
return timeAtLastMark;
}
public void setEstimatedTime(long estimatedTime) {
this.estimatedTime = estimatedTime;
/**
* Sets the time at which the boat reached the previous mark to a specified time.
* @param timeAtLastMark Time at which boat passed previous mark.
*/
public void setTimeAtLastMark(ZonedDateTime timeAtLastMark) {
this.timeAtLastMark = timeAtLastMark;
}
}

@ -33,7 +33,7 @@ public class Constants {
* Frame periods are multiplied by this to get the amount of time a single frame represents.
* E.g., frame period = 20ms, scale = 5, frame represents 20 * 5 = 100ms, and so boats are simulated for 100ms, even though only 20ms actually occurred.
*/
public static final int RaceTimeScale = 1;
public static final int RaceTimeScale = 25;
/**
* The race pre-start time, in milliseconds. 3 minutes.
@ -49,9 +49,22 @@ public class Constants {
/**
* The number of milliseconds in one hour.
* <br>
* Multiply by this factor to convert milliseconds to hours.
* <br>
* Divide by this factor to convert hours to milliseconds.
*/
public static long OneHourMilliseconds = 1 * 60 * 60 * 1000;
/**
* The number of seconds in one hour.
* <br>
* Multiply by this factor to convert seconds to hours.
* <br>
* Divide by this factor to convert hours to seconds.
*/
public static long OneHourSeconds = 1 * 60 * 60;
}

@ -299,6 +299,7 @@ public class GPSCoordinate {
*/
public static List<GPSCoordinate> getShrinkBoundary(List<GPSCoordinate> boundary) {
//TODO shrinkDistance should be a parameter. Also the code should be refactored to be smaller/simpler.
double shrinkDistance = 50d;
List<GPSCoordinate> shrunkBoundary = new ArrayList<>(boundary.size());

@ -1,5 +1,7 @@
package shared.model;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.Enums.RaceTypeEnum;
import network.Messages.LatestMessages;
@ -116,7 +118,7 @@ public abstract class Race implements Runnable {
/**
* The number of frames per second we generated over the last 1 second period.
*/
private int lastFps = 0;
private IntegerProperty lastFps = new SimpleIntegerProperty(0);
/**
* The time, in milliseconds, since we last reset our {@link #currentFps} counter.
@ -280,12 +282,46 @@ public abstract class Race implements Runnable {
}
/**
* Returns the RaceDataSource used for the race.
* @return The RaceDataSource used for the race.
*/
public RaceDataSource getRaceDataSource() {
return raceDataSource;
}
/**
* Returns the number of legs in the race.
* @return The number of legs in the race.
*/
public int getLegCount() {
//We minus one, as we have added an extra "dummy" leg.
return legs.size() - 1;
}
/**
* Returns the race boundary.
* @return The race boundary.
*/
public List<GPSCoordinate> getBoundary() {
return boundary;
}
/**
* Returns the number of frames generated per second.
* @return Frames per second.
*/
public int getFps() {
return lastFps;
return lastFps.getValue();
}
/**
* Returns the fps property.
* @return The fps property.
*/
public IntegerProperty fpsProperty() {
return lastFps;
}
@ -302,7 +338,7 @@ public abstract class Race implements Runnable {
//If we have reached 1 second period, snapshot the framerate and reset.
if (this.lastFpsResetTime > 1000) {
this.lastFps = this.currentFps;
this.lastFps.set(this.currentFps);
this.currentFps = 0;
this.lastFpsResetTime = 0;

@ -72,8 +72,9 @@ public class RaceClock implements Runnable {
/**
* Format string used for current time.
* TODO probably need to fix this.
*/
private String currentTimeFormat = "'Starting time:' HH:mm dd/MM/YYYY";
private String currentTimeFormat = "'Current time:' HH:mm dd/MM/YYYY";
/**
* Format string used for duration before it has started.

@ -7,14 +7,14 @@ import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.AnchorPane;
import seng302.Model.Boat;
import visualiser.model.VisualiserBoat;
import java.net.URL;
import java.util.ResourceBundle;
/**
* Finish Screen for when the race finishs.
* Finish Screen for when the race finishes.
*/
public class FinishController extends Controller {
@ -22,41 +22,67 @@ public class FinishController extends Controller {
AnchorPane finishWrapper;
@FXML
TableView<Boat> boatInfoTable;
TableView<VisualiserBoat> boatInfoTable;
@FXML
TableColumn<Boat, String> boatRankColumn;
TableColumn<VisualiserBoat, String> boatRankColumn;
@FXML
TableColumn<Boat, String> boatNameColumn;
TableColumn<VisualiserBoat, String> boatNameColumn;
@FXML
Label raceWinnerLabel;
/**
* The boats to display on the table.
*/
private ObservableList<VisualiserBoat> boats;
/**
* Ctor.
*/
public FinishController() {
}
@Override
public void initialize(URL location, ResourceBundle resources){
}
/**
* Sets up the finish table
* @param boats Boats to display
*/
private void setFinishTable(ObservableList<Boat> boats){
private void setFinishTable(ObservableList<VisualiserBoat> boats) {
this.boats = boats;
//Set contents.
boatInfoTable.setItems(boats);
boatNameColumn.setCellValueFactory(cellData -> cellData.getValue().getName());
//Name.
boatNameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
//Rank/position.
boatRankColumn.setCellValueFactory(cellData -> cellData.getValue().positionProperty());
//Winner label.
raceWinnerLabel.setText("Winner: "+ boatNameColumn.getCellObservableValue(0).getValue());
raceWinnerLabel.setWrapText(true);
}
@Override
public void initialize(URL location, ResourceBundle resources){
}
/**
* Display the table
* @param boats boats to display on the table.
*/
public void enterFinish(ObservableList<Boat> boats){
public void enterFinish(ObservableList<VisualiserBoat> boats){
finishWrapper.setVisible(true);
setFinishTable(boats);
}

@ -15,11 +15,22 @@ import java.util.ResourceBundle;
* Controller that everything is overlayed onto. This makes it so that changing scenes does not resize your stage.
*/
public class MainController extends Controller {
@FXML private StartController startController;
@FXML private RaceController raceController;
@FXML private ConnectionController connectionController;
@FXML private FinishController finishController;
/**
* Ctor.
*/
public MainController() {
}
/**
* Transitions from the StartController screen (displays pre-race information) to the RaceController (displays the actual race).
* @param visualiserInput The object used to read packets from the race server.
@ -29,10 +40,18 @@ public class MainController extends Controller {
raceController.startRace(visualiserInput, visualiserRace);
}
/**
* Transitions from the server selection screen to the pre-race lobby for a given server.
* @param socket The server to read data from.
*/
public void enterLobby(Socket socket) {
startController.enterLobby(socket);
}
/**
* Transitions from the {@link RaceController} screen to the {@link FinishController} screen.
* @param boats The boats to display on the finish screen.
*/
public void enterFinish(ObservableList<VisualiserBoat> boats) {
finishController.enterFinish(boats);
}

@ -1,7 +1,9 @@
package visualiser.Controllers;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.chart.LineChart;
@ -10,13 +12,14 @@ import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import network.Messages.Enums.RaceStatusEnum;
import shared.model.Leg;
import visualiser.app.VisualiserInput;
import shared.model.RaceClock;
import visualiser.model.Sparkline;
import visualiser.model.VisualiserBoat;
import visualiser.model.VisualiserRace;
import visualiser.model.*;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.ResourceBundle;
/**
@ -34,8 +37,15 @@ public class RaceController extends Controller {
*/
private VisualiserRace visualiserRace;
private ResizableRaceCanvas raceMap;
private ResizableRaceMap raceBoundaries;
/**
* An additional observable list of boats. This is used by the table view, to allow it to sort boats without effecting the race's own list of boats.
*/
private ObservableList<VisualiserBoat> tableBoatList;
/**
* The canvas that draws the race.
*/
private ResizableRaceCanvas raceCanvas;
/**
* The sparkline graph.
@ -54,12 +64,13 @@ public class RaceController extends Controller {
@FXML private TableView<VisualiserBoat> boatInfoTable;
@FXML private TableColumn<VisualiserBoat, String> boatPlacingColumn;
@FXML private TableColumn<VisualiserBoat, String> boatTeamColumn;
@FXML private TableColumn<VisualiserBoat, String> boatMarkColumn;
@FXML private TableColumn<VisualiserBoat, String> boatSpeedColumn;
@FXML private TableColumn<VisualiserBoat, Leg> boatMarkColumn;
@FXML private TableColumn<VisualiserBoat, Number> boatSpeedColumn;
@FXML private LineChart<Number, Number> sparklineChart;
@FXML private AnchorPane annotationPane;
/**
* Ctor.
*/
@ -68,10 +79,54 @@ public class RaceController extends Controller {
@Override
public void initialize(URL location, ResourceBundle resources) {
initialiseFpsToggle();
}
/**
* Initialises the various UI components to listen to the {@link #visualiserRace}.
*/
private void initialiseRace() {
//Fps display.
initialiseFps(this.visualiserRace);
//Need to add the included arrow pane to the arrowPane container.
initialiseArrow();
//Information table.
initialiseInfoTable(this.visualiserRace);
//Sparkline.
initialiseSparkline(this.visualiserRace);
//Race canvas.
initialiseRaceCanvas(this.visualiserRace);
//Race timezone label.
initialiseRaceTimezoneLabel(this.visualiserRace);
//Race clock.
initialiseRaceClock(this.visualiserRace);
//Start the race animation timer.
raceTimer();
}
/**
* Initialises the frame rate functionality. This allows for toggling the frame rate, and connect the fps label to the race's fps property.
* @param visualiserRace The race to connect the fps label to.
*/
private void initialiseFps(VisualiserRace visualiserRace) {
//On/off toggle.
initialiseFpsToggle();
//Label value.
initialiseFpsLabel(visualiserRace);
}
/**
* Initialises a listener for the fps toggle.
*/
@ -90,149 +145,240 @@ public class RaceController extends Controller {
}
/**
* Updates the ResizableRaceCanvas (raceMap) with most recent data
*
* @param boats boats that are to be displayed in the race
* @param boatMarkers Markers for boats
* @see ResizableRaceCanvas
* Initialises the fps label to update when the race fps changes.
* @param visualiserRace The race to monitor the frame rate of.
*/
public void updateMap(ObservableList<Boat> boats, ObservableList<Marker> boatMarkers) {
raceMap.setBoats(boats);
raceMap.setBoatMarkers(boatMarkers);
raceMap.update();
raceBoundaries.draw();
//stop if the visualiser is no longer running
private void initialiseFpsLabel(VisualiserRace visualiserRace) {
visualiserRace.fpsProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> this.FPS.setText("FPS: " + newValue.toString()));
});
}
/**
* Updates the array listened by the TableView (boatInfoTable) that displays the boat information.
*
* Initialises the information table view to listen to a given race.
* @param race Race to listen to.
*/
public void setInfoTable(VisualiserRace race) {
boatInfoTable.setItems(race.getStartingBoats());
boatTeamColumn.setCellValueFactory(cellData -> cellData.getValue().getName());
boatSpeedColumn.setCellValueFactory(cellData -> cellData.getValue().getVelocityProp());
boatMarkColumn.setCellValueFactory(cellData -> cellData.getValue().getCurrentLegName());
boatPlacingColumn.setCellValueFactory(cellData -> cellData.getValue().positionProperty());
}
public void initialiseInfoTable(VisualiserRace race) {
//Copy list of boats.
this.tableBoatList = FXCollections.observableArrayList(race.getBoats());
//Set up table.
boatInfoTable.setItems(this.tableBoatList);
//Set up each column.
//Name.
boatTeamColumn.setCellValueFactory(
cellData -> cellData.getValue().nameProperty() );
//Speed.
boatSpeedColumn.setCellValueFactory(
cellData -> cellData.getValue().currentSpeedProperty() );
//Kind of ugly, but allows for formatting an observed speed.
boatSpeedColumn.setCellFactory(
//Callback object.
new Callback<TableColumn<VisualiserBoat, Number>, TableCell<VisualiserBoat, Number>>() {
//Callback function.
@Override
public TableCell<VisualiserBoat, Number> call(TableColumn<VisualiserBoat, Number> param) {
//We return a table cell that populates itself with a Number, and formats it.
return new TableCell<VisualiserBoat, Number>(){
//Function to update the cell text.
@Override
protected void updateItem(Number item, boolean empty) {
if (item != null) {
super.updateItem(item, empty);
setText(String.format("%.2fkn", item.doubleValue()));
}
}
};
}
} );
//Last mark.
boatMarkColumn.setCellValueFactory(
cellData -> cellData.getValue().legProperty() );
//Kind of ugly, but allows for turning an observed Leg into a string.
boatMarkColumn.setCellFactory(
//Callback object.
new Callback<TableColumn<VisualiserBoat, Leg>, TableCell<VisualiserBoat, Leg>>() {
//Callback function.
@Override
public TableCell<VisualiserBoat, Leg> call(TableColumn<VisualiserBoat, Leg> param) {
//We return a table cell that populates itself with a Leg's name.
return new TableCell<VisualiserBoat, Leg>(){
//Function to update the cell text.
@Override
protected void updateItem(Leg item, boolean empty) {
if (item != null) {
super.updateItem(item, empty);
setText(item.getName());
}
}
};
}
} );
//Current place within race.
boatPlacingColumn.setCellValueFactory(
cellData -> cellData.getValue().positionProperty() );
/**
* Creates and sets initial display for Sparkline for race positions.
* @param boats boats to display on the sparkline
*/
public void createSparkLine(ObservableList<VisualiserBoat> boats){
sparkline = new Sparkline(boats, legNum, sparklineChart);
}
/**
* Updates the sparkline to display current boat positions.
* @param boatsInRace used for current boat positions.
* Initialises the {@link Sparkline}, and listens to a specified {@link VisualiserRace}.
* @param race The race to listen to.
*/
public void updateSparkline(ObservableList<VisualiserBoat> boatsInRace){
sparkline.updateSparkline(boatsInRace);
private void initialiseSparkline(VisualiserRace race) {
this.sparkline = new Sparkline(race.getBoats(), race.getLegCount(), this.sparklineChart);
}
/**
* Displays a specified race.
* @param visualiserInput Object used to read packets from server.
* @param visualiserRace Object modelling the race.
* Initialises the {@link ResizableRaceCanvas}, provides the race to read data from.
* @param race Race to read data from.
*/
public void startRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace) {
private void initialiseRaceCanvas(VisualiserRace race) {
this.visualiserInput = visualiserInput;
this.visualiserRace = visualiserRace;
//Create canvas.
raceCanvas = new ResizableRaceCanvas(race, arrow.getChildren().get(0));
legNum = visualiserInput.getCourse().getLegs().size()-1;
//Set properties.
raceCanvas.setMouseTransparent(true);
raceCanvas.widthProperty().bind(canvasBase.widthProperty());
raceCanvas.heightProperty().bind(canvasBase.heightProperty());
makeArrow();
//Draw it and show it.
raceCanvas.draw();
raceCanvas.setVisible(true);
raceMap = new ResizableRaceCanvas(visualiserInput.getCourse());
raceMap.setMouseTransparent(true);
raceMap.widthProperty().bind(canvasBase.widthProperty());
raceMap.heightProperty().bind(canvasBase.heightProperty());
raceMap.draw();
raceMap.setVisible(true);
raceMap.setArrow(arrow.getChildren().get(0));
//Add to scene.
canvasBase.getChildren().add(0, raceCanvas);
canvasBase.getChildren().add(0, raceMap);
raceBoundaries = new ResizableRaceMap(visualiserInput.getCourse());
raceBoundaries.setMouseTransparent(true);
raceBoundaries.widthProperty().bind(canvasBase.widthProperty());
raceBoundaries.heightProperty().bind(canvasBase.heightProperty());
raceBoundaries.draw();
raceBoundaries.setVisible(true);
}
canvasBase.getChildren().add(0, raceBoundaries);
race.setVisible(true);
/**
* Intialises the race time zone label with the race's time zone.
* @param race The race to get time zone from.
*/
private void initialiseRaceTimezoneLabel(VisualiserRace race) {
timeZone.setText(race.getRaceClock().getTimeZone());
}
timeZone.setText(raceClock.getTimeZone());
/**
* Initialises the race clock to listen to the specified race.
* @param race The race to listen to.
*/
private void initialiseRaceClock(VisualiserRace race) {
//RaceClock.duration isn't necessarily being changed in the javaFX thread, so we need to runlater the update.
raceClock.durationProperty().addListener((observable, oldValue, newValue) -> {
race.getRaceClock().durationProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
timer.setText(newValue);
});
});
}
this.raceClock = raceClock;
raceMap.setRaceClock(raceClock);
//TODO move this list of colors somewhere more sensible.
StreamedRace newRace = new StreamedRace(visualiserInput, colours, this);
/**
* Displays a specified race.
* @param visualiserInput Object used to read packets from server.
* @param visualiserRace Object modelling the race.
*/
public void startRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace) {
initializeFPS();
this.visualiserInput = visualiserInput;
this.visualiserRace = visualiserRace;
// set up annotation displays
new Annotations(annotationPane, raceMap);
initialiseRace();
new Thread((newRace)).start();
//Display this controller.
race.setVisible(true);
// set up annotation displays
new Annotations(annotationPane, raceCanvas);
}
/**
* Finish Race View
* Transition from the race view to the finish view.
* @param boats boats there are in the race.
*/
public void finishRace(ObservableList<Boat> boats){
public void finishRace(ObservableList<VisualiserBoat> boats){
race.setVisible(false);
parent.enterFinish(boats);
}
/**
* Set the value for the fps label
*
* @param fps fps that the label will be updated to
* Adds the included arrow pane (see arrow.fxml) to the arrowPane (see race.fxml).
*/
public void setFrames(String fps) {
FPS.setText((fps));
private void initialiseArrow() {
arrowPane.getChildren().add(arrow);
}
/**
* Set up FPS display at bottom of screen
* Timer which monitors the race.
*/
private void initializeFPS() {
showFPS.setVisible(true);
showFPS.selectedProperty().addListener((ov, old_val, new_val) -> {
if (showFPS.isSelected()) {
FPS.setVisible(true);
} else {
FPS.setVisible(false);
}
});
}
private void raceTimer() {
new AnimationTimer() {
@Override
public void handle(long arg0) {
private void makeArrow() {
arrowPane.getChildren().add(arrow);
}
//Get the current race status.
RaceStatusEnum raceStatus = visualiserRace.getRaceStatusEnum();
//If the race has finished, go to finish view.
if (raceStatus == RaceStatusEnum.FINISHED) {
//Stop this timer.
stop();
//Hide this, and display the finish controller.
finishRace(visualiserRace.getBoats());
public RaceClock getRaceClock() {
return raceClock;
} else {
//Otherwise, render the canvas.
raceCanvas.drawRace();
//Sort the tableview. Doesn't automatically work for all columns.
boatInfoTable.sort();
//Update sparkline. TODO this should simply observe the boats.
sparkline.updateSparkline(visualiserRace.getBoats());
}
}
}.start();
}
}

@ -121,6 +121,7 @@ public class StartController extends Controller implements Observer {
//Create race.
this.visualiserRace = new VisualiserRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, this.colors);
new Thread(this.visualiserRace).start();
//Initialise the boat table.
@ -193,6 +194,8 @@ public class StartController extends Controller implements Observer {
*/
private void initialiseRaceClockStartTime(VisualiserRace visualiserRace) {
raceStartLabel.setText(visualiserRace.getRaceClock().getStartingTimeString());
visualiserRace.getRaceClock().startingTimeProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
raceStartLabel.setText(newValue);

@ -28,7 +28,7 @@ public class App extends Application {
System.exit(0);
}
});
FXMLLoader loader = new FXMLLoader(getClass().getResource("/scenes/main.fxml"));
FXMLLoader loader = new FXMLLoader(getClass().getResource("/visualiser/scenes/main.fxml"));
Parent root = loader.load();
Scene scene = new Scene(root, 1200, 800);
stage.setScene(scene);

@ -220,6 +220,8 @@ public class VisualiserInput implements Runnable {
lastHeartbeatSequenceNum = heartbeat.getSequenceNumber();
//System.out.println("HeartBeat Message! " + lastHeartbeatSequenceNum);
}
break;
}
//RaceStatus.
@ -233,12 +235,15 @@ public class VisualiserInput implements Runnable {
this.latestMessages.setBoatStatus(boatStatus);
}
break;
}
//DisplayTextMessage.
case DISPLAYTEXTMESSAGE: {
//System.out.println("Display Text Message");
//No decoder for this.
break;
}
//XMLMessage.
@ -249,21 +254,25 @@ public class VisualiserInput implements Runnable {
this.latestMessages.setXMLMessage(xmlMessage);
break;
}
//RaceStartStatus.
case RACESTARTSTATUS: {
//System.out.println("Race Start Status Message");
break;
}
//YachtEventCode.
case YACHTEVENTCODE: {
//YachtEventCode yachtEventCode = (YachtEventCode) message;
//YachtEventCode yachtEventCode = (YachtEventCode) message;
//System.out.println("Yacht Event Code!");
//No decoder for this.
//System.out.println("Yacht Event Code!");
//No decoder for this.
break;
}
//YachtActionCode.
@ -273,6 +282,7 @@ public class VisualiserInput implements Runnable {
//System.out.println("Yacht Action Code!");
// No decoder for this.
break;
}
//ChatterText.
@ -282,6 +292,7 @@ public class VisualiserInput implements Runnable {
//System.out.println("Chatter Text Message!");
//No decoder for this.
break;
}
//BoatLocation.
@ -301,6 +312,8 @@ public class VisualiserInput implements Runnable {
//If the map _doesn't_ already contain a message for this boat, insert the message.
this.latestMessages.setBoatLocation(boatLocation);
}
break;
}
//MarkRounding.
@ -323,6 +336,7 @@ public class VisualiserInput implements Runnable {
this.latestMessages.setMarkRounding(markRounding);
}
break;
}
//CourseWinds.
@ -333,6 +347,7 @@ public class VisualiserInput implements Runnable {
this.latestMessages.setCourseWinds(courseWinds);
break;
}
//AverageWind.
@ -343,11 +358,14 @@ public class VisualiserInput implements Runnable {
this.latestMessages.setAverageWind(averageWind);
break;
}
//Unrecognised message.
default: {
System.out.println("Broken Message!");
break;
}
}

@ -14,13 +14,13 @@ import java.util.Map;
/**
* Class that processes user selected annotation visibility options to
* display the requested information on the
* {@link seng302.Model.ResizableRaceMap ResizbleRaceMap}. These are displayed
* via the {@link seng302.Controllers.RaceController RaceController}. <br>
* Annotation options for a {@link seng302.Model.Boat Boat} include: its name,
* {@link ResizableRaceCanvas}. These are displayed
* via the {@link visualiser.Controllers.RaceController}. <br>
* Annotation options for a {@link VisualiserBoat} include: its name,
* abbreviation, speed, the time since it passed the last
* {@link seng302.Model.Marker Marker}, estimated time to the next marker,
* {@link shared.model.Mark}, estimated time to the next marker,
* and a path it has travelled made up of
* {@link seng302.Model.TrackPoint TrackPoint}s.
* {@link TrackPoint}s.
*/
public class Annotations {
private ResizableRaceCanvas raceMap;
@ -107,7 +107,7 @@ public class Annotations {
if (old_val != new_val) {
raceMap.toggleAnnoName();
storeCurrentAnnotationState(nameCheckAnno, new_val);
raceMap.update();
raceMap.draw();
}
});
@ -117,7 +117,7 @@ public class Annotations {
if (old_val != new_val) {
raceMap.toggleAnnoAbbrev();
storeCurrentAnnotationState(abbrevCheckAnno, new_val);
raceMap.update();
raceMap.draw();
}
});
@ -127,7 +127,7 @@ public class Annotations {
if (old_val != new_val) {
raceMap.toggleBoatPath();
storeCurrentAnnotationState(pathCheckAnno, new_val);
raceMap.update();
raceMap.draw();
}
});
@ -137,7 +137,7 @@ public class Annotations {
if (old_val != new_val) {
raceMap.toggleAnnoSpeed();
storeCurrentAnnotationState(speedCheckAnno, new_val);
raceMap.update();
raceMap.draw();
}
});
@ -147,7 +147,7 @@ public class Annotations {
if (old_val != new_val) {
raceMap.toggleAnnoTime();
storeCurrentAnnotationState(timeCheckAnno, new_val);
raceMap.update();
raceMap.draw();
}
});
@ -157,7 +157,7 @@ public class Annotations {
if (old_val != new_val) {
raceMap.toggleAnnoEstTime();
storeCurrentAnnotationState(estTimeCheckAnno, new_val);
raceMap.update();
raceMap.draw();
}
});
}
@ -191,7 +191,7 @@ public class Annotations {
for (Map.Entry<String, CheckBox> checkBox : checkBoxes.entrySet()){
checkBox.getValue().setSelected(false);
}
raceMap.update();
raceMap.draw();
buttonChecked = noBtn;
prevBtnChecked = hideBtn;
selectShow = true;
@ -206,7 +206,7 @@ public class Annotations {
checkBox.getValue().setSelected(
annoShownBeforeHide.get(checkBox.getKey()));
}
raceMap.update();
raceMap.draw();
buttonChecked = noBtn;
prevBtnChecked = showBtn;
}
@ -226,7 +226,7 @@ public class Annotations {
}
else { checkBox.getValue().setSelected(false); }
}
raceMap.update();
raceMap.draw();
buttonChecked = noBtn;
prevBtnChecked = partialBtn;
selectShow = true;
@ -243,6 +243,7 @@ public class Annotations {
(importantAnno.get(checkBox.getKey()));
}
}
raceMap.draw();
buttonChecked = noBtn;
prevBtnChecked = importantBtn;
selectShow = true;

@ -2,12 +2,20 @@ package visualiser.model;
/**
* It is a coordinate representing a location on the
* {@link seng302.Model.ResizableRaceMap ResizableRaceMap}.
* It has been converted from a {@link seng302.GPSCoordinate GPSCoordinate}
* {@link ResizableRaceMap}.
* It has been converted from a {@link shared.model.GPSCoordinate}
* to display objects in their relative positions.
*/
public class GraphCoordinate {
/**
* X (horizontal) coordinate.
*/
private final int x;
/**
* Y (vertical) coordinate.
*/
private final int y;
/**
@ -21,6 +29,7 @@ public class GraphCoordinate {
this.y = y;
}
/**
* Returns the X coordinate.
*

@ -28,9 +28,10 @@ public class RaceConnection {
*/
@SuppressWarnings("unused")
public boolean check() {
//TODO the connection needs to be moved to its own thread, so it doesn't block fx thread.
InetSocketAddress i = new InetSocketAddress(hostname.get(), port);
try (Socket s = new Socket()){
s.connect(i, 5000);
s.connect(i, 750);//TODO this should be at least a second or two, once moved to its own thread
status.set("Ready");
return true;
} catch (IOException e) {}

@ -1,40 +1,75 @@
package visualiser.model;
import shared.model.GPSCoordinate;
/**
* The base size of the map to be used for the
* {@link seng302.Model.ResizableRaceMap ResizableRaceMap} and
* {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas}. It is used
* to convert {@link seng302.GPSCoordinate GPSCoordinate}s to relative
* {@link seng302.GraphCoordinate GraphCoordinate}s.
* {@link ResizableRaceMap} and
* {@link ResizableRaceCanvas}. It is used
* to convert {@link shared.model.GPSCoordinate}s to relative
* {@link GraphCoordinate}s.
*/
public class RaceMap {
private final double x1;
private final double x2;
private final double y1;
private final double y2;
private int width, height;
/**
* The longitude of the left side of the map.
*/
private final double longLeft;
/**
* The longitude of the right side of the map.
*/
private final double longRight;
/**
* The latitude of the top side of the map.
*/
private final double latTop;
/**
* The latitude of the bottom side of the map.
*/
private final double latBottom;
/**
* The width, in pixels, of the map.
*/
private int width;
/**
* The height, in pixels, of the map.
*/
private int height;
/**
* Constructor Method.
*
* @param x1 Longitude of the top left point.
* @param y1 Latitude of the top left point.
* @param x2 Longitude of the top right point.
* @param y2 Latitude of the top right point.
* @param latTop Latitude of the top left point.
* @param longLeft Longitude of the top left point.
* @param latBottom Latitude of the top right point.
* @param longRight Longitude of the top right point.
* @param width width that the Canvas the race is to be drawn on is.
* @param height height that the Canvas the race is to be drawn on is.
*/
public RaceMap(double y1, double x1, double y2, double x2, int height, int width) {
this.x1 = x1;
this.x2 = x2;
this.y1 = y1;
this.y2 = y2;
public RaceMap(double latTop, double longLeft, double latBottom, double longRight, int width, int height) {
//Long/lat edges.
this.longLeft = longLeft;
this.longRight = longRight;
this.latTop = latTop;
this.latBottom = latBottom;
//Pixel sizes.
this.width = width;
this.height = height;
}
/**
* Converts GPS coordinates to coordinates for container
* Converts GPS coordinates to coordinates for container.
* It is assumed that the provided GPSCoordinate will always be within the GPSCoordinate boundaries of the RaceMap.
*
* @param lat GPS latitude
* @param lon GPS longitude
@ -42,20 +77,50 @@ public class RaceMap {
* @see GraphCoordinate
*/
private GraphCoordinate convertGPS(double lat, double lon) {
int difference = Math.abs(width - height);
int size = width;
//Calculate the width/height, in gps coordinates, of the map.
double longWidth = longRight - longLeft;
double latHeight = latBottom - latTop;
//Calculate the distance between the specified coordinate and the edge of the map.
double longDelta = lon - longLeft;
double latDelta = lat - latTop;
//Calculate the proportion along horizontally, from the left, the coordinate should be.
double longProportion = longDelta / longWidth;
//Calculate the proportion along vertically, from the top, the coordinate should be.
double latProportion = latDelta / latHeight;
//Check which pixel dimension of our map is smaller. We use this to ensure that any rendered stuff retains its correct aspect ratio, and that everything is visible on screen.
int smallerDimension = Math.min(width, height);
//Calculate the x and y pixel coordinates.
//We take the complement of latProportion to flip it.
int x = (int) (longProportion * smallerDimension);
int y = (int) ((1 - latProportion) * smallerDimension);
//Because we try to maintain the correct aspect ratio, we will end up with "spare" pixels along the larger dimension (e.g., width 800, height 600, 200 extra pixels along width).
int extraPixels = Math.abs(width - height);
//We therefore "center" the coordinates along this larger dimension, by adding half of the extra pixels.
if (width > height) {
size = height;
return new GraphCoordinate((int) ((size * (lon - x1) / (x2 - x1)) + difference / 2), (int) (size - (size * (lat - y1) / (y2 - y1))));
x += extraPixels / 2;
} else {
return new GraphCoordinate((int) (size * (lon - x1) / (x2 - x1)), (int) ((size - (size * (lat - y1) / (y2 - y1))) + difference / 2));
y += extraPixels / 2;
}
//return new GraphCoordinate((int) (width * (lon - x1) / (x2 - x1)), (int) (height - (height * (lat - y1) / (y2 - y1))));
//Finally, create the GraphCoordinate.
GraphCoordinate graphCoordinate = new GraphCoordinate(x, y);
return graphCoordinate;
}
/**
* Converts the GPS Coordinate to GraphCoordinates
* Converts the GPS Coordinate to GraphCoordinate.
* It is assumed that the provided GPSCoordinate will always be within the GPSCoordinate boundaries of the RaceMap.
*
* @param coordinate GPSCoordinate representation of Latitude and Longitude.
* @return GraphCoordinate that the GPS is coordinates are to be displayed on the map.
@ -66,10 +131,18 @@ public class RaceMap {
return convertGPS(coordinate.getLatitude(), coordinate.getLongitude());
}
/**
* Sets the width, in pixels, of our RaceMap.
* @param width The new width, in pixels, of the RaceMap.
*/
public void setWidth(int width) {
this.width = width;
}
/**
* Sets the height, in pixels, of our RaceMap.
* @param height The new height, in pixels, of the RaceMap.
*/
public void setHeight(int height) {
this.height = height;
}

@ -7,46 +7,44 @@ import javafx.scene.canvas.GraphicsContext;
* The abstract class for the resizable race canvases.
*/
public abstract class ResizableCanvas extends Canvas {
/**
* The {@link GraphicsContext} to draw to.
*/
protected final GraphicsContext gc;
/**
* Ctor.
*/
public ResizableCanvas(){
this.gc = this.getGraphicsContext2D();
// Redraw canvas when size changes.
widthProperty().addListener(evt -> draw());
heightProperty().addListener(evt -> draw());
}
abstract void draw();
/**
* Set the Canvas to resizable.
*
* @return That the Canvas is resizable.
* Draws desired contents onto the canvas.
* Subclasses implement this to decide what to draw.
*/
public abstract void draw();
@Override
public boolean isResizable() {
return true;
}
/**
* Returns the preferred width of the Canvas
*
* @param width of canvas
* @return Returns the width of the Canvas
*/
@Override
public double prefWidth(double width) {
return getWidth();
}
/**
* Returns the preferred height of the Canvas
*
* @param height of canvas
* @return Returns the height of the Canvas
*/
@Override
public double prefHeight(double height) {
return getHeight();

@ -1,107 +1,161 @@
package visualiser.model;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.transform.Rotate;
import seng302.Mock.StreamedCourse;
import seng302.RaceDataSource;
import seng302.RaceMap;
import network.Messages.Enums.BoatStatusEnum;
import shared.dataInput.RaceDataSource;
import shared.model.GPSCoordinate;
import shared.model.Mark;
import shared.model.RaceClock;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.List;
/**
* This JavaFX Canvas is used to update and display details for a
* {@link seng302.RaceMap RaceMap} via the
* {@link seng302.Controllers.RaceController RaceController}.<br>
* {@link RaceMap} via the
* {@link visualiser.Controllers.RaceController}.<br>
* It fills it's parent and cannot be downsized. <br>
* Details displayed include:
* {@link seng302.Model.Boat Boats} (and their
* {@link seng302.Model.TrackPoint TrackPoint}s),
* {@link seng302.Model.Marker Markers}, a
* {@link seng302.Model.RaceClock RaceClock}, a wind direction arrow and
* various user selected {@link seng302.Model.Annotations Annotations}.
* {@link VisualiserBoat}s (and their
* {@link TrackPoint}s),
* {@link shared.model.Mark}s, a
* {@link RaceClock}, a wind direction arrow and
* various user selected {@link Annotations}.
*/
public class ResizableRaceCanvas extends ResizableCanvas {
/**
* The RaceMap used for converting GPSCoordinates to GraphCoordinates.
*/
private RaceMap map;
private List<Boat> boats;
private List<Marker> boatMarkers;
/**
* The race we read data from and draw.
*/
private VisualiserRace visualiserRace;
/**
* The background of the race.
* We render the background whenever the race boundary changes, or the screen size changes.
*/
private Image background;
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 List<Color> colours;
private final List<Marker> markers;
private final RaceDataSource raceData;
private Map<Integer, Color> boatColours = new HashMap<>();
/**
* The wind arrow node.
*/
private Node arrow;
private RaceClock raceClock;
public ResizableRaceCanvas(RaceDataSource raceData) {
/**
* Constructs a {@link ResizableRaceCanvas} using a given {@link VisualiserRace}.
* @param visualiserRace The race that data is read from in order to be drawn.
* @param arrow The wind arrow's node.
*/
public ResizableRaceCanvas(VisualiserRace visualiserRace, Node arrow) {
super();
this.visualiserRace = visualiserRace;
this.arrow = arrow;
RaceDataSource raceData = visualiserRace.getRaceDataSource();
double lat1 = raceData.getMapTopLeft().getLatitude();
double long1 = raceData.getMapTopLeft().getLongitude();
double lat2 = raceData.getMapBottomRight().getLatitude();
double long2 = raceData.getMapBottomRight().getLongitude();
setMap(new RaceMap(lat1, long1, lat2, long2, (int) getWidth(), (int) getHeight()));
this.map = new RaceMap(
lat1, long1, lat2, long2,
(int) getWidth(), (int) getHeight() );
this.markers = raceData.getMarkers();
makeColours();
this.raceData = raceData;
}
/**
* Sets the boats that are to be displayed in this race.
*
* @param boats in race
* Toggle name display in annotation
*/
public void toggleAnnoName() {
annoName = !annoName;
}
/**
* Toggle boat path display in annotation
*/
public void setBoats(List<Boat> boats) {
this.boats = boats;
mapBoatColours();
public void toggleBoatPath() {
annoPath = !annoPath;
}
public void toggleAnnoEstTime() {
annoEstTime = !annoEstTime;
}
/**
* Sets the boat markers that are to be displayed in this race.
*
* @param boatMarkers in race
* Toggle boat time display in annotation
*/
public void toggleAnnoTime() {
annoTimeSinceLastMark = !annoTimeSinceLastMark;
}
/**
* Toggle abbreviation display in annotation
*/
public void toggleAnnoAbbrev() {
annoAbbrev = !annoAbbrev;
}
/**
* Toggle speed display in annotation
*/
public void setBoatMarkers(ObservableList<Marker> boatMarkers) {
this.boatMarkers = boatMarkers;
public void toggleAnnoSpeed() {
annoSpeed = !annoSpeed;
}
/**
* Sets the RaceMap that the RaceCanvas is to be displaying for.
* Rotates things on the canvas Note: this must be called in between gc.save() and gc.restore() else they will rotate everything
*
* @param map race map
* @param degrees Bearing degrees to rotate.
* @param px Pivot point x of rotation.
* @param py Pivot point y of rotation.
*/
private void setMap(RaceMap map) {
this.map = map;
private void rotate(double degrees, double px, double py) {
Rotate r = new Rotate(degrees, px, py);
gc.setTransform(r.getMxx(), r.getMyx(), r.getMxy(), r.getMyy(), r.getTx(), r.getTy());
}
private void displayBoat(Boat boat, double angle, Color colour) {
if (boat.getCurrentPosition() != null) {
GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
double[] x = {pos.getX() - 6, pos.getX(), pos.getX() + 6};
double[] y = {pos.getY() + 12, pos.getY() - 12, pos.getY() + 12};
gc.setFill(colour);
gc.save();
rotate(angle, pos.getX(), pos.getY());
gc.fillPolygon(x, y, 3);
gc.restore();
}
/**
* Draws a circle with a given diameter, centred on a given graph coordinate.
* @param center The center coordinate of the circle.
* @param diameter The diameter of the circle.
*/
private void drawCircle(GraphCoordinate center, double diameter) {
//The graphCoordinates are for the center of the point, so we offset them to get the corner coordinate.
gc.fillOval(
center.getX() - (diameter / 2),
center.getY() - (diameter / 2),
diameter, diameter );
}
/**
@ -110,63 +164,48 @@ public class ResizableRaceCanvas extends ResizableCanvas {
* @param graphCoordinateA Starting Point of the line in GraphCoordinate.
* @param graphCoordinateB End Point of the line in GraphCoordinate.
* @param paint Colour the line is to coloured.
* @see GraphCoordinate
* @see Color
* @see Paint
*/
private void displayLine(GraphCoordinate graphCoordinateA, GraphCoordinate graphCoordinateB, Paint paint) {
private void drawLine(GraphCoordinate graphCoordinateA, GraphCoordinate graphCoordinateB, Paint paint) {
gc.setStroke(paint);
gc.setFill(paint);
gc.fillOval(graphCoordinateA.getX() - 3, graphCoordinateA.getY() - 3, 6, 6);
gc.fillOval(graphCoordinateB.getX() - 3, graphCoordinateB.getY() - 3, 6, 6);
gc.strokeLine(graphCoordinateA.getX(), graphCoordinateA.getY(), graphCoordinateB.getX(), graphCoordinateB.getY());
}
/**
* Display a point on the Canvas
*
* @param graphCoordinate Coordinate that the point is to be displayed at.
* @param paint Colour that the boat is to be coloured.
* @see GraphCoordinate
* @see Paint
* @see Color
*/
private void displayPoint(GraphCoordinate graphCoordinate, Paint paint) {
gc.setFill(paint);
gc.fillOval(graphCoordinate.getX(), graphCoordinate.getY(), 10, 10);
}
double endPointDiameter = 6;
/**
* Displays an arrow representing wind direction on the Canvas
*
* @param angle Angle that the arrow is to be facing in degrees 0 degrees = North (Up).
* @see GraphCoordinate
*/
private void displayWindArrow(double angle) {
angle = angle % 360;
//Draw first end-point.
drawCircle(graphCoordinateA, endPointDiameter);
// show direction wind is coming from
if (angle<180){angle = angle + 180;}
else {angle = angle - 180;}
//Draw second end-point.
drawCircle(graphCoordinateB, endPointDiameter);
//Draw line between them.
gc.strokeLine(
graphCoordinateA.getX(),
graphCoordinateA.getY(),
graphCoordinateB.getX(),
graphCoordinateB.getY() );
if (arrow != null && arrow.getRotate() != angle) {
arrow.setRotate(angle);
}
}
/**
* Rotates things on the canvas Note: this must be called in between gc.save() and gc.restore() else they will rotate everything
* Display a point on the Canvas. It has a diameter of 10 pixels.
*
* @param angle Bearing angle to rotate at in degrees
* @param px Pivot point x of rotation.
* @param py Pivot point y of rotation.
* @param graphCoordinate Coordinate that the point is to be displayed at.
* @param paint Paint to use for the point.
*/
private void rotate(double angle, double px, double py) {
Rotate r = new Rotate(angle, px, py);
gc.setTransform(r.getMxx(), r.getMyx(), r.getMxy(), r.getMyy(), r.getTx(), r.getTy());
private void drawPoint(GraphCoordinate graphCoordinate, Paint paint) {
//Set paint.
gc.setFill(paint);
double pointDiameter = 10;
//Draw the point.
drawCircle(graphCoordinate, pointDiameter);
}
/**
* Display given name and speed of boat at a graph coordinate
*
@ -174,220 +213,348 @@ public class ResizableRaceCanvas extends ResizableCanvas {
* @param abbrev abbreviation of the boat name
* @param speed speed of the boat
* @param coordinate coordinate the text appears
* @param timeSinceLastMark time since the last mark was passed
* @param timeToNextMark The time until the boat reaches the next mark.
* @param timeSinceLastMark The time since the boat passed the last mark.
*/
private void displayText(String name, String abbrev, double speed, GraphCoordinate coordinate, String estTime, ZonedDateTime timeSinceLastMark) {
private void drawText(String name, String abbrev, double speed, GraphCoordinate coordinate, String timeToNextMark, String timeSinceLastMark) {
//The text to draw. Built during the function.
String text = "";
//Check name toggle value
if (annoName){
//Draw name if annotation is enabled.
if (annoName) {
text += String.format("%s ", name);
}
//Check abbreviation toggle value
if (annoAbbrev){
//Draw abbreviation/country if annotation is enabled.
if (annoAbbrev) {
text += String.format("%s ", abbrev);
}
//Check speed toggle value
//Draw speed if annotation is enabled.
if (annoSpeed){
text += String.format("%.2fkn ", speed);
}
//Draw time to reach next mark if annotation is enabled.
if (annoEstTime) {
text += estTime;
text += timeToNextMark;
}
//Check time since last mark toggle value
if(annoTimeSinceLastMark){
Duration timeSince = Duration.between(timeSinceLastMark, raceClock.getTime());
text += String.format(" %ds ", timeSince.getSeconds());
//Draw time since last mark if annotation is enabled.
if(annoTimeSinceLastMark) {
text += timeSinceLastMark;
}
//String text = String.format("%s, %2$.2fkn", name, speed);
//Offset by 20 pixels horizontally.
long xCoord = coordinate.getX() + 20;
long yCoord = coordinate.getY();
//If the text would extend out of the canvas (to the right), move it left.
if (xCoord + (text.length() * 7) >= getWidth()) {
xCoord -= text.length() * 7;
}
if (yCoord - (text.length() * 2) <= 0) {
yCoord += 30;
}
//Draw text.
gc.fillText(text, xCoord, yCoord);
}
/**
* Draws race map with up to date data.
* Draws the label for a given boat. Includes name, abbreviation, speed, time since mark, and time to next mark.
* @param boat The boat to draw text for.
*/
public void update() {
this.draw();
this.updateBoats();
private void drawBoatText(VisualiserBoat boat) {
drawText(
boat.getName(),
boat.getCountry(),
boat.getCurrentSpeed(),
this.map.convertGPS(boat.getCurrentPosition()),
boat.getTimeToNextMarkFormatted(this.visualiserRace.getRaceClock().getCurrentTime()),
boat.getTimeSinceLastMarkFormatted(this.visualiserRace.getRaceClock().getCurrentTime()) );
}
/**
* Draw race markers
* Draws all of the boats on the canvas.
*/
private void drawMarkers() {
for(Marker marker: markers) {
GraphCoordinate mark1 = this.map.convertGPS(marker.getMark1());
// removed drawing of lines between the marks as only
// the start and finish line should have a line drawn
if(marker.isCompoundMark()) {
GraphCoordinate mark2 = this.map.convertGPS(marker.getMark2());
displayPoint(mark1, Color.LIMEGREEN);
displayPoint(mark2, Color.LIMEGREEN);
} else {
displayPoint(mark1, Color.GREEN);
private void drawBoats() {
for (VisualiserBoat boat : visualiserRace.getBoats()) {
//Draw the boat.
drawBoat(boat);
//Only draw wake if they are currently racing.
if (boat.getStatus() == BoatStatusEnum.RACING) {
drawWake(boat);
}
//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());
}
//Draw boat label.
drawBoatText(boat);
//Draw track.
drawTrack(boat);
}
}
/**
* Draws the Race Map
* Draws a given boat on the canvas.
* @param boat The boat to draw.
*/
public void draw() {
private void drawBoat(VisualiserBoat boat) {
//The position may be null if we haven't received any BoatLocation messages yet.
if (boat.getCurrentPosition() != null) {
double width = getWidth();
double height = getHeight();
//Convert position to graph coordinate.
GraphCoordinate pos = this.map.convertGPS(boat.getCurrentPosition());
gc.clearRect(0, 0, width, height);
//The x coordinates of each vertex of the boat.
double[] x = {
pos.getX() - 6,
pos.getX(),
pos.getX() + 6 };
if (map == null) {
return;//TODO this should return a exception in the future
}
this.map.setHeight((int) height);
this.map.setWidth((int) width);
//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.
//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.setLineWidth(2);
updateBoats();
drawMarkers();
//display wind direction arrow - specify origin point and angle - angle now set to random angle
if (raceData instanceof StreamedCourse) {
displayWindArrow(((StreamedCourse) raceData).getWindDirection());
} else {
displayWindArrow(150);
}
}
/**
* Toggle name display in annotation
* Draws the wake for a given boat.
* @param boat Boat to draw wake for.
*/
public void toggleAnnoName() {
annoName = !annoName;
private void drawWake(VisualiserBoat boat) {
//Calculate either end of wake line.
GraphCoordinate wakeFrom = this.map.convertGPS(boat.getCurrentPosition());
GraphCoordinate wakeTo = this.map.convertGPS(boat.getWake());
//Draw.
drawLine(wakeFrom, wakeTo, boat.getColor());
}
/**
* Toggle boat path display in annotation
* Displays an arrow representing wind direction on the Canvas.
* This function accepts a wind-to bearing, but displays a wind-from bearing.
*
* @param angle Angle that the arrow is to be facing in degrees 0 degrees = North (Up).
* @see GraphCoordinate
*/
public void toggleBoatPath() {
annoPath = !annoPath;
}
private void displayWindArrow(double angle) {
public void toggleAnnoEstTime() {
annoEstTime = !annoEstTime;
//We need to display wind-from, so add 180 degrees.
angle += 180d;
//Get it within [0, 360).
while (angle >= 360d) {
angle -= 360d;
}
//Rotate the wind arrow.
if (arrow != null && arrow.getRotate() != angle) {
arrow.setRotate(angle);
}
}
/**
* Toggle boat time display in annotation
* Draws all of the {@link Mark}s on the canvas.
*/
public void toggleAnnoTime() { annoTimeSinceLastMark = !annoTimeSinceLastMark;}
private void drawMarks() {
for (Mark mark : this.visualiserRace.getMarks()) {
drawMark(mark);
}
}
/**
* Toggle abbreviation display in annotation
* Draws a given mark on the canvas.
* @param mark The mark to draw.
*/
public void toggleAnnoAbbrev() {
annoAbbrev = !annoAbbrev;
private void drawMark(Mark mark) {
//Calculate screen position.
GraphCoordinate mark1 = this.map.convertGPS(mark.getPosition());
//Draw.
drawPoint(mark1, Color.LIMEGREEN);
}
/**
* Toggle speed display in annotation
* Draws the Race Map.
* Called when the canvas is resized.
*/
public void toggleAnnoSpeed() {
annoSpeed = !annoSpeed;
public void draw() {
//Clear canvas.
clear();
//Update our RaceMap using new canvas size.
this.map.setWidth((int) getWidth());
this.map.setHeight((int) getHeight());
//Redraw the boundary.
redrawBoundaryImage();
//Draw the race.
drawRace();
}
/**
* Draws boats while race in progress, when leg heading is set.
* Clears the canvas.
*/
private void updateBoats() {
if (boats != null) {
if (boatColours.size() < boats.size()) mapBoatColours();
for (Boat boat : boats) {
boolean finished = boat.getCurrentLeg().getName().equals("Finish") || boat.getCurrentLeg().getName().equals("DNF");
boolean isStart = boat.isStarted();
int sourceID = boat.getSourceID();
if (!finished && isStart) {
displayBoat(boat, boat.getHeading(), boatColours.get(sourceID));
GraphCoordinate wakeFrom = this.map.convertGPS(boat.getCurrentPosition());
GraphCoordinate wakeTo = this.map.convertGPS(boat.getWake());
displayLine(wakeFrom, wakeTo, boatColours.get(sourceID));
} else if (!isStart) {
displayBoat(boat, boat.getHeading(), boatColours.get(sourceID));
} else {
displayBoat(boat, 0, boatColours.get(sourceID));
}
if (Duration.between(boat.getTimeSinceLastMark(), raceClock.getTime()).getSeconds() < 0) {
boat.setTimeSinceLastMark(raceClock.getTime());
}
//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.isStarted() == false) {
boat.setTimeSinceLastMark(raceClock.getTime());
}
displayText(boat.toString(), boat.getAbbrev(), boat.getVelocity(), this.map.convertGPS(boat.getCurrentPosition()), boat.getFormattedEstTime(), boat.getTimeSinceLastMark());
//TODO this needs to be fixed.
drawTrack(boat, boatColours.get(sourceID));
}
}
private void clear() {
gc.clearRect(0, 0, getWidth(), getHeight());
}
/**
* Draws all track points for a given boat. Colour is set by boat, opacity by track point.
* @param boat whose track is displayed
* @param colour The color to use for the track.
* @see seng302.Model.TrackPoint
* Draws the race boundary, and saves the image to {@link #background}.
* You should call {@link #clear()} before calling this.
*/
private void drawTrack(Boat boat, Color colour) {
if (annoPath) {
for (TrackPoint point : boat.getTrack()) {
GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate());
gc.setFill(new Color(colour.getRed(), colour.getGreen(), colour.getBlue(), point.getAlpha()));
gc.fillOval(scaledCoordinate.getX(), scaledCoordinate.getY(), 5, 5);
}
private void redrawBoundaryImage() {
//Prepare to draw.
gc.setLineWidth(1);
gc.setFill(Color.AQUA);
//Calculate the screen coordinates of the boundary.
List<GPSCoordinate> boundary = this.visualiserRace.getBoundary();
double[] xpoints = new double[boundary.size()];
double[] ypoints = new double[boundary.size()];
//For each boundary coordinate.
for (int i = 0; i < boundary.size(); i++) {
//Convert.
GraphCoordinate coord = map.convertGPS(boundary.get(i));
//Use.
xpoints[i] = coord.getX();
ypoints[i] = coord.getY();
}
//Draw the boundary.
gc.fillPolygon(xpoints, ypoints, xpoints.length);
//Render boundary to image.
this.background = snapshot(null, null);
}
/**
* makes colours
* Draws the race.
* Called once per frame, and on canvas resize.
*/
private void makeColours() {
colours = new ArrayList<>(Arrays.asList(
Color.BLUEVIOLET,
Color.BLACK,
Color.RED,
Color.ORANGE,
Color.DARKOLIVEGREEN,
Color.LIMEGREEN,
Color.PURPLE,
Color.DARKGRAY,
Color.YELLOW
));
}
public void drawRace() {
gc.setLineWidth(2);
clear();
//Race boundary.
drawBoundary();
//Boats.
drawBoats();
//Marks.
drawMarks();
//Wind arrow. This rotates the wind arrow node.
displayWindArrow(this.visualiserRace.getWindDirection().degrees());
public void setArrow(Node arrow) {
this.arrow = arrow;
}
public void setRaceClock(RaceClock raceClock) {
this.raceClock = raceClock;
/**
* Draws the race boundary image onto the canvas.
* See {@link #background}.
*/
private void drawBoundary() {
gc.drawImage(this.background, 0, 0);
}
private void mapBoatColours() {
int currentColour = 0;
for (Boat boat : boats) {
if (!boatColours.containsKey(boat.getSourceID())) {
boatColours.put(boat.getSourceID(), colours.get(currentColour));
/**
* Draws all track points for a given boat. Colour is set by boat, opacity by track point.
* This checks if {@link #annoPath} is enabled.
* @param boat The boat to draw tracks for.
* @see TrackPoint
*/
private void drawTrack(VisualiserBoat boat) {
//Check that track points are enabled.
if (this.annoPath) {
//Apply the boat color.
gc.setFill(boat.getColor());
//Draw each TrackPoint.
for (TrackPoint point : boat.getTrack()) {
//Convert the GPSCoordinate to a screen coordinate.
GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate());
//Draw a circle for the trackpoint.
gc.fillOval(scaledCoordinate.getX(), scaledCoordinate.getY(), point.getDiameter(), point.getDiameter());
}
currentColour = (currentColour + 1) % colours.size();
}
}
}

@ -1,93 +0,0 @@
package visualiser.model;
import javafx.scene.paint.Color;
import seng302.GPSCoordinate;
import seng302.RaceDataSource;
import seng302.RaceMap;
import java.util.List;
/**
* This JavaFX Canvas is used to generate the size of a
* {@link seng302.RaceMap RaceMap} using co-ordinates from a
* {@link seng302.RaceDataSource RaceDataSource}. This is done via the
* {@link seng302.Controllers.RaceController RaceController}.
*/
public class ResizableRaceMap extends ResizableCanvas {
private RaceMap map;
private final List<GPSCoordinate> raceBoundaries;
private double[] xpoints = {};
private double[] ypoints = {};
/**
* Constructor
* @param raceData Race which it is taking its information to be displayed from
*/
public ResizableRaceMap(RaceDataSource raceData){
super();
raceBoundaries = raceData.getBoundary();
double lat1 = raceData.getMapTopLeft().getLatitude();
double long1 = raceData.getMapTopLeft().getLongitude();
double lat2 = raceData.getMapBottomRight().getLatitude();
double long2 = raceData.getMapBottomRight().getLongitude();
setMap(new RaceMap(lat1, long1, lat2, long2, (int) getWidth(), (int) getHeight()));
//draw();
}
/**
* Sets the map race that it is supposed to be viewing.
* @param map the map to be set
*/
private void setMap(RaceMap map) {
this.map = map;
}
/**
* Draw boundary of the race.
*/
private void drawBoundaries() {
if (this.raceBoundaries == null) {
return;
}
gc.setFill(Color.AQUA);
setRaceBoundCoordinates();
gc.setLineWidth(1);
gc.fillPolygon(xpoints, ypoints, xpoints.length);
}
/**
* Sets the coordinately of the race boundaries
*/
private void setRaceBoundCoordinates() {
xpoints = new double[this.raceBoundaries.size()];
ypoints = new double[this.raceBoundaries.size()];
for (int i = 0; i < raceBoundaries.size(); i++) {
GraphCoordinate coord = map.convertGPS(raceBoundaries.get(i));
xpoints[i] = coord.getX();
ypoints[i] = coord.getY();
}
}
/**
* Draw update for the canvas
*/
public void draw(){
double width = getWidth();
double height = getHeight();
gc.clearRect(0, 0, width, height);
if (map == null) {
return;//TODO this should return a exception in the future
}
this.map.setHeight((int) height);
this.map.setWidth((int) width);
gc.setLineWidth(2);
drawBoundaries();
}
}

@ -7,27 +7,44 @@ import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
/**
* Class to process and modify a sparkline display. This display keeps visual
* track of {@link seng302.Model.Boat Boats}s in a race and their current
* placing position as they complete each {@link seng302.Model.Leg Leg} by
* passing a course {@link seng302.Model.Marker Marker}. <br>
* track of {@link VisualiserBoat}s in a race and their current
* placing position as they complete each {@link shared.model.Leg} by
* passing a course {@link shared.model.Mark}. <br>
* This sparkline is displayed using the
* {@link seng302.Controllers.RaceController RaceController}.
* {@link visualiser.Controllers.RaceController}.
*/
public class Sparkline {
private ArrayList<String> colours;
private ArrayList<VisualiserBoat> startBoats = new ArrayList<>();
private Map<Integer, String> boatColours = new HashMap<>();
/**
* The boats to observe.
*/
private ObservableList<VisualiserBoat> boats;
/**
* The number of legs in the race.
* Used to correctly scale the linechart.
*/
private Integer legNum;
//TODO comment
private Integer sparkLineNumber = 0;
/**
* The linchart to plot sparklines on.
*/
@FXML LineChart<Number, Number> sparklineChart;
/**
* The x axis of the sparkline chart.
*/
@FXML NumberAxis xAxis;
/**
* The y axis of the sparkline chart.
*/
@FXML NumberAxis yAxis;
@ -37,17 +54,18 @@ public class Sparkline {
* @param legNum total number of legs in the race
* @param sparklineChart javaFX LineChart for the sparkline
*/
public Sparkline(ObservableList<Boat> boats, Integer legNum,
public Sparkline(ObservableList<VisualiserBoat> boats, Integer legNum,
LineChart<Number,Number> sparklineChart) {
this.boats = boats;
this.sparklineChart = sparklineChart;
this.legNum = legNum;
this.yAxis = (NumberAxis)sparklineChart.getYAxis();
this.xAxis = (NumberAxis)sparklineChart.getXAxis();
startBoats.addAll(boats);
this.yAxis = (NumberAxis) sparklineChart.getYAxis();
this.xAxis = (NumberAxis) sparklineChart.getXAxis();
makeColours();
mapBoatColours();
createSparkline();
//TODO refactor
//what we probably want to do is listen to the boats list, and when it updates, we update an observable list of series with new data. the linechart listens to the list of series.
}
@ -60,13 +78,13 @@ public class Sparkline {
// NOTE: Y axis is in negatives to display correct positions
// all boats start in 'last' place
for (int i=0; i<startBoats.size(); i++){
for (int i = 0; i< boats.size(); i++){
XYChart.Series<Number, Number> series = new XYChart.Series();
series.getData().add(new XYChart.Data(0, -startBoats.size()));
series.getData().add(new XYChart.Data(0, -startBoats.size()));
series.getData().add(new XYChart.Data(0, -boats.size()));
series.getData().add(new XYChart.Data(0, -boats.size()));
sparklineChart.getData().add(series);
sparklineChart.getData().get(i).getNode().setStyle("-fx-stroke: " +
""+boatColours.get(startBoats.get(i).getSourceID())+";");
"" + colourToHex(boats.get(i).getColor()) + ";");
}
sparklineChart.setCreateSymbols(false);
@ -76,11 +94,11 @@ public class Sparkline {
xAxis.setTickMarkVisible(false);
xAxis.setTickLabelsVisible(false);
xAxis.setMinorTickVisible(false);
xAxis.setUpperBound((startBoats.size()+1)*legNum);
xAxis.setTickUnit((startBoats.size()+1)*legNum);
xAxis.setUpperBound((boats.size()+1)*legNum);
xAxis.setTickUnit((boats.size()+1)*legNum);
// set y axis details
yAxis.setLowerBound(-(startBoats.size()+1));
yAxis.setLowerBound(-(boats.size()+1));
yAxis.setUpperBound(0);
yAxis.setAutoRanging(false);
yAxis.setLabel("Position in Race");
@ -93,7 +111,7 @@ public class Sparkline {
@Override
public String toString(Number value) {
if ((Double)value == 0.0
|| (Double)value < -startBoats.size()){
|| (Double)value < -boats.size()){
return "";
}
else {
@ -103,6 +121,7 @@ public class Sparkline {
});
}
/**
* Updates the sparkline to display current boat positions.
* New points are plotted to represent each boat when required.
@ -113,8 +132,8 @@ public class Sparkline {
sparkLineNumber++;
for (int i = boatsInRace.size() - 1; i >= 0; i--){
for (int j = startBoats.size() - 1; j >= 0; j--){
if (boatsInRace.get(i)==startBoats.get(j)){
for (int j = boats.size() - 1; j >= 0; j--){
if (boatsInRace.get(i)== boats.get(j)){
// when a boat is on its first leg
if (boatsInRace.get(i).getCurrentLeg().getLegNumber()==0){
@ -147,20 +166,12 @@ public class Sparkline {
}
}
private void makeColours() {
colours = new ArrayList<>(Arrays.asList(
colourToHex(Color.BLUEVIOLET),
colourToHex(Color.BLACK),
colourToHex(Color.RED),
colourToHex(Color.ORANGE),
colourToHex(Color.DARKOLIVEGREEN),
colourToHex(Color.LIMEGREEN),
colourToHex(Color.PURPLE),
colourToHex(Color.DARKGRAY),
colourToHex(Color.YELLOW)
));
}
/**
* Converts a color to a hex string, starting with a {@literal #} symbol.
* @param color
* @return
*/
private String colourToHex(Color color) {
return String.format( "#%02X%02X%02X",
(int)( color.getRed() * 255 ),
@ -168,13 +179,4 @@ public class Sparkline {
(int)( color.getBlue() * 255 ) );
}
private void mapBoatColours() {
int currentColour = 0;
for (Boat boat : startBoats) {
if (!boatColours.containsKey(boat.getSourceID())) {
boatColours.put(boat.getSourceID(), colours.get(currentColour));
}
currentColour = (currentColour + 1) % colours.size();
}
}
}

@ -7,17 +7,39 @@ import shared.model.GPSCoordinate;
* A TrackPoint is a point plotted to display the track a
* {@link VisualiserBoat Boat} has travelled in a race. <br>
* TrackPoints are displayed on a
* {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas}, via the
* {@link seng302.Controllers.RaceController RaceController}. <br>
* {@link ResizableRaceCanvas}, via the
* {@link visualiser.Controllers.RaceController}. <br>
* Track points can be made visible or hidden via the RaceController's
* {@link seng302.Model.Annotations Annotations}.
* {@link Annotations}.
*/
public class TrackPoint {
/**
* The {@link GPSCoordinate} this {@link TrackPoint} corresponds to.
*/
private final GPSCoordinate coordinate;
/**
* The time the track point was created at, in milliseconds since unix epoch.
*/
private final long timeAdded;
/**
* The period of time, in milliseconds, over which the track point's alpha should diminish to a floor value of {@link #minAlpha}.
*/
private final long expiry;
/**
* The minimum alpha to draw the track point with.
*/
private final double minAlpha;
/**
* The diameter to draw the track point with.
*/
private final double diameter;
/**
* Creates a new track point with fixed GPS coordinates and time, to reach minimum opacity on expiry.
*
@ -29,9 +51,12 @@ public class TrackPoint {
this.coordinate = coordinate;
this.timeAdded = timeAdded;
this.expiry = expiry;
this.minAlpha = 0.1;
this.diameter = 5d;
}
/**
* Gets the position of the point on physical race map.
*
@ -44,10 +69,23 @@ public class TrackPoint {
/**
* Gets opacity of point scaled by age in proportion to expiry, between 1 and minimum opacity inclusive.
*
* @return greater of minimum opacity and scaled opacity
* @return Greater of minimum opacity and scaled opacity.
*/
public double getAlpha() {
return Double.max(minAlpha, 1.0 - (double) (System.currentTimeMillis() - timeAdded) / expiry);
//Calculate how much the alpha should be attenuated by elapsed time.
//Elapsed time.
long elapsedTime = System.currentTimeMillis() - this.timeAdded;
//Proportion of expiry period that has elapsed. (E.g., 2.5 means that 5 times the period has elapsed.)
double elapsedProportion = ((double) elapsedTime) / this.expiry;
//As the alpha diminishes from 1 down to a floor, we take the complement of this value. This may be negative.
double calculatedAlpha = 1.0 - elapsedProportion;
//We then take the max of the minAlpha and calculatedAlpha so that it doesn't go past our floor value.
return Double.max(this.minAlpha, calculatedAlpha);
}
/**
@ -58,4 +96,13 @@ public class TrackPoint {
public long getTimeAdded() {
return timeAdded;
}
/**
* Returns the diameter to draw the track point with.
* @return The diameter to draw the track point with.
*/
public double getDiameter() {
return diameter;
}
}

@ -1,17 +1,16 @@
package visualiser.model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.paint.Color;
import network.Messages.Enums.BoatStatusEnum;
import org.geotools.referencing.GeodeticCalculator;
import shared.model.Azimuth;
import shared.model.Boat;
import shared.model.Constants;
import shared.model.GPSCoordinate;
import java.awt.geom.Point2D;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a Boat on the visualiser side of a race.
@ -27,33 +26,40 @@ public class VisualiserBoat extends Boat {
/**
* The collection of trackpoints generated for the boat.
*/
private final Queue<TrackPoint> track = new ConcurrentLinkedQueue<>();
private final List<TrackPoint> track = new ArrayList<>();
/**
* The next time, in milliseconds since unix epoch, at which we may create a new track point.
*/
private long nextValidTime = 0;
private ZonedDateTime timeSinceLastMark;
/**
* The minimum period of time, in milliseconds, between the creation of each track point.
*/
private static final long trackPointTimeInterval = 5000;
/**
* The number of track points that should be created before fully diminishing the alpha of a given track point.
*/
private static final int trackPointLimit = 10;
/**
* The boat's color.
*/
private Color color;
/**
* Boat initializer which keeps all of the information of the boat.
*
* @param sourceID The source ID of the boat.
* @param name Name of the Boat.
* @param abbrev The team/country abbreviation of the boat.
* @param color The color of the boat.
* Scalar used to scale the boat's wake.
*/
public VisualiserBoat(int sourceID, String name, String abbrev, Color color) {
super(sourceID, name, abbrev);
private static final double wakeScale = 5;
this.color = color;
}
/**
* Constructs a mock boat object from a given boat and polars table.
* Constructs a boat object from a given boat and color.
*
* @param boat The boat to convert into a MockBoat.
* @param color The color of the boat.
@ -73,41 +79,62 @@ public class VisualiserBoat extends Boat {
* @return GPSCoordinate of wake endpoint.
*/
public GPSCoordinate getWake() {
double reverseHeading = getBearing().degrees() - 180;
double wakeScale = 5;
double distance = wakeScale * getCurrentSpeed();
GeodeticCalculator calc = new GeodeticCalculator();
calc.setStartingGeographicPoint(
new Point2D.Double(getCurrentPosition().getLongitude(), getCurrentPosition().getLatitude())
);
calc.setDirection(reverseHeading, distance);
Point2D endpoint = calc.getDestinationGeographicPoint();
return new GPSCoordinate(endpoint.getY(), endpoint.getX());
//Calculate the reverse bearing of the boat, and convert it to an azimuth.
Azimuth reverseAzimuth = Azimuth.fromDegrees(getBearing().degrees() - 180d);
//Calculate the distance, in meters, of the wake.
//We currently use boat's speed, in meters per second, to calculate the wake length. Could maybe move the knot -> m/s calculation somewhere else.
double speedKnots = getCurrentSpeed();
double speedMetersPerHour = speedKnots * Constants.NMToMetersConversion;
double speedMetersPerSecond = speedMetersPerHour / Constants.OneHourSeconds;
double wakeDistanceMeters = speedMetersPerSecond * this.wakeScale;
//Calculate the new coordinate.
GPSCoordinate wakeCoordinate = GPSCoordinate.calculateNewPosition(getCurrentPosition(), wakeDistanceMeters, reverseAzimuth);
return wakeCoordinate;
}
/**
* Adds a new point to boat's track.
* @param coordinate of point on track
* Attempts to add a new point to boat's track.
* It only adds a new point if the boat is still racing (see {@link #getStatus()} and {@link BoatStatusEnum#RACING}), and if at least {@link #trackPointTimeInterval} milliseconds have occurred.
* @param coordinate The {@link GPSCoordinate} of the trackpoint.
* @see TrackPoint
*/
public void addTrackPoint(GPSCoordinate coordinate) {
Boolean added = System.currentTimeMillis() >= nextValidTime;
//Get current time.
long currentTime = System.currentTimeMillis();
if (added && (this.getStatus() == BoatStatusEnum.RACING)) {
float trackPointTimeInterval = 5000;
nextValidTime = currentTime + (long) trackPointTimeInterval;
int TRACK_POINT_LIMIT = 10;
track.add(new TrackPoint(coordinate, currentTime, TRACK_POINT_LIMIT * (long) trackPointTimeInterval));
//Check if enough time has passed to create a new track point.
Boolean canBeAdded = currentTime >= nextValidTime;
//If it has, and if we are still racing, create the point.
if (canBeAdded && (getStatus() == BoatStatusEnum.RACING)) {
//Calculate the period of time that it should take the track point to diminish over. We essentially allow for trackPointLimit number of track points to be created before it should fade out.
long expiryPeriod = trackPointTimeInterval * trackPointLimit;
//Create and add point.
TrackPoint trackPoint = new TrackPoint(coordinate, currentTime, expiryPeriod);
track.add(trackPoint);
//Update the nextValidTime for the next track point.
nextValidTime = currentTime + trackPointTimeInterval;
}
}
/**
* Returns the boat's sampled track between start of race and current time.
* @return queue of track points
* @return The list of track points.
* @see TrackPoint
*/
public Queue<TrackPoint> getTrack() {
public List<TrackPoint> getTrack() {
return track;
}
@ -128,27 +155,59 @@ public class VisualiserBoat extends Boat {
return getName();
}
public ZonedDateTime getTimeSinceLastMark() {
return timeSinceLastMark;
}
public void setTimeSinceLastMark(ZonedDateTime timeSinceLastMark) {
this.timeSinceLastMark = timeSinceLastMark;
}
public String getFormattedEstTime() {
if (getEstimatedTime() < 0) {
/**
* Returns the time until the boat will reach the next mark, as a string.
* @param currentTime The current race time.
* @return The time delta until the boat reaches the next mark.
*/
public String getTimeToNextMarkFormatted(ZonedDateTime currentTime) {
//Calculate time delta.
Duration timeUntil = Duration.between(currentTime, getEstimatedTimeAtNextMark());
//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 " -";
}
if (getEstimatedTime() <= 60) {
return " " + getEstimatedTime() + "s";
if (secondsUntil <= 60) {
//If less than 1 minute, display seconds only.
return " " + secondsUntil + "s";
} else {
long seconds = getEstimatedTime() % 60;
long minutes = (getEstimatedTime() - seconds) / 60;
//Otherwise display minutes and seconds.
long seconds = secondsUntil % 60;
long minutes = (secondsUntil - seconds) / 60;
return String.format(" %dm %ds", minutes, seconds);
}
}
/**
* Returns the time since the boat passed the previous mark, as a string.
* @param currentTime The current race time.
* @return The time delta since the boat passed the previous mark.
*/
public String getTimeSinceLastMarkFormatted(ZonedDateTime currentTime) {
if (getTimeAtLastMark() != null) {
//Calculate time delta.
Duration timeSince = Duration.between(getTimeAtLastMark(), currentTime);
//Format it.
return String.format(" %ds ", timeSince.getSeconds());
} else {
return " -";
}
}
}

@ -1,7 +1,6 @@
package visualiser.model;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.paint.Color;
@ -15,8 +14,9 @@ import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import shared.dataInput.RegattaDataSource;
import shared.model.*;
import visualiser.Controllers.RaceController;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -94,6 +94,14 @@ public class VisualiserRace extends Race {
}
/**
* 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.
@ -120,6 +128,9 @@ public class VisualiserRace extends Race {
visualiserBoats.add(visualiserBoat);
//Next color.
colorIndex++;
}
return visualiserBoats;
@ -139,7 +150,7 @@ public class VisualiserRace extends Race {
for (VisualiserBoat boat : boats) {
boat.setCurrentLeg(startingLeg);
boat.setTimeSinceLastMark(this.raceClock.getCurrentTime());
boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
}
@ -183,15 +194,21 @@ public class VisualiserRace extends Race {
boat.setBearing(Bearing.fromDegrees(boatLocation.getHeadingDegrees()));
//Time until next mark.
boat.setEstimatedTime(convertEstTime(boatStatus.getEstTimeAtNextMark(), boatLocation.getTime()));
boat.setEstimatedTimeAtNextMark(raceClock.getLocalTime(boatStatus.getEstTimeAtNextMark()));
//Speed.
boat.setCurrentSpeed(boatLocation.getBoatSOG() / Constants.KnotsToMMPerSecond);
//Boat status.
BoatStatusEnum boatStatusEnum = BoatStatusEnum.fromByte(boatStatus.getBoatStatus());
boat.setStatus(boatStatusEnum);
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.
@ -200,18 +217,18 @@ public class VisualiserRace extends Race {
if (legNumber >= 1 && legNumber < legs.size()) {
if (boat.getCurrentLeg() != legs.get(legNumber)) {
boat.setCurrentLeg(legs.get(legNumber));
boat.setTimeSinceLastMark(this.raceClock.getCurrentTime());
boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
}
}
//Add a track point.
if (boatStatusEnum == BoatStatusEnum.RACING) {
//Attempt to add a track point.
if (newBoatStatusEnum == BoatStatusEnum.RACING) {
boat.addTrackPoint(boat.getCurrentPosition());
}
//Set finish time if boat finished.
if (boatStatusEnum == BoatStatusEnum.FINISHED || legNumber == this.legs.size()) {
if (newBoatStatusEnum == BoatStatusEnum.FINISHED || legNumber == this.legs.size()) {
boat.setTimeFinished(boatLocation.getTime());
boat.setStatus(BoatStatusEnum.FINISHED);
@ -297,16 +314,14 @@ public class VisualiserRace extends Race {
new AnimationTimer() {
//final long timeRaceStarted = System.currentTimeMillis(); //start time of loop
int fps = 0; //init fps value
//long timeCurrent = System.currentTimeMillis(); //current time
long lastFrameTime = System.currentTimeMillis();
@Override
public void handle(long arg0) {
//totalTimeElapsed = System.currentTimeMillis() - timeRaceStarted;
//Calculate the frame period.
long currentFrameTime = System.currentTimeMillis();
long framePeriod = currentFrameTime - lastFrameTime;
//Update racing boats.
updateBoats(boats, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap());
@ -325,6 +340,10 @@ public class VisualiserRace extends Race {
stop();
}
lastFrameTime = currentFrameTime;
//Increment fps.
incrementFps(framePeriod);
}
}.start();
@ -357,7 +376,7 @@ public class VisualiserRace extends Race {
//If they're on the same leg, we need to compare time to finish leg.
if (legNumberDelta == 0) {
return (int) (b.getEstimatedTime() - a.getEstimatedTime());
return (int) Duration.between(b.getEstimatedTimeAtNextMark(), a.getEstimatedTimeAtNextMark()).toMillis();
} else {
return legNumberDelta;
}
@ -381,13 +400,18 @@ public class VisualiserRace extends Race {
* Takes an estimated time an event will occur, and converts it to the
* number of seconds before the event will occur.
*
* @param estTimeMillis estimated time in milliseconds
* @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;
int estElapsedSecs = Math.round(estElapsedMillis/1000);
//Convert milliseconds to seconds.
int estElapsedSecs = Math.round(estElapsedMillis / 1000);
return estElapsedSecs;
}

@ -4,7 +4,7 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?>
<AnchorPane fx:id="connectionWrapper" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0" prefWidth="780.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="seng302.Controllers.ConnectionController">
<AnchorPane fx:id="connectionWrapper" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="600.0" prefWidth="780.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.ConnectionController">
<children>
<GridPane fx:id="connection" prefHeight="600.0" prefWidth="780.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<columnConstraints>

@ -3,7 +3,7 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?>
<AnchorPane fx:id="finishWrapper" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" visible="false" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="seng302.Controllers.FinishController">
<AnchorPane fx:id="finishWrapper" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" visible="false" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.FinishController">
<children>
<GridPane fx:id="start" alignment="CENTER" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="600.0" prefWidth="780.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<columnConstraints>

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane fx:id="main" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="seng302.Controllers.MainController">
<AnchorPane fx:id="main" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.MainController">
<children>
<fx:include fx:id="race" source="race.fxml" />
<fx:include fx:id="start" source="start.fxml" />

@ -6,7 +6,7 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?>
<SplitPane fx:id="race" dividerPositions="0.7" prefHeight="431.0" prefWidth="610.0" visible="false" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="seng302.Controllers.RaceController">
<SplitPane fx:id="race" dividerPositions="0.7" prefHeight="431.0" prefWidth="610.0" visible="false" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.RaceController">
<items>
<GridPane fx:id="canvasBase">
<columnConstraints>

@ -3,7 +3,7 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?>
<AnchorPane fx:id="startWrapper" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" visible="false" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="seng302.Controllers.StartController">
<AnchorPane fx:id="startWrapper" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" visible="false" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="visualiser.Controllers.StartController">
<children>
<GridPane fx:id="start" prefHeight="600.0" prefWidth="780.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<columnConstraints>
@ -43,4 +43,4 @@
</children>
</GridPane>
</children>
</AnchorPane>
</AnchorPane>

Loading…
Cancel
Save