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) { } catch (XMLReaderException | InvalidBoatDataException | InvalidRaceDataException | InvalidRegattaDataException e) {
e.printStackTrace(); 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 * 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 * @return String containing edited xml
*/ */
private String getRaceXMLAtCurrentTime(String raceXML) { private String getRaceXMLAtCurrentTime(String raceXML) {
@ -102,9 +103,9 @@ public class Event {
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
ZonedDateTime creationTime = ZonedDateTime.now(); 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; return raceXML;

@ -1,7 +1,6 @@
package mock.model; package mock.model;
import javafx.animation.AnimationTimer; import javafx.animation.AnimationTimer;
import mock.app.MockOutput;
import network.Messages.BoatLocation; import network.Messages.BoatLocation;
import network.Messages.BoatStatus; import network.Messages.BoatStatus;
import network.Messages.Enums.BoatStatusEnum; import network.Messages.Enums.BoatStatusEnum;
@ -14,6 +13,9 @@ import network.Messages.Enums.RaceStatusEnum;
import shared.dataInput.RegattaDataSource; import shared.dataInput.RegattaDataSource;
import shared.model.*; import shared.model.*;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.*; import java.util.*;
import static java.lang.Math.cos; import static java.lang.Math.cos;
@ -239,8 +241,8 @@ public class MockRace extends Race {
*/ */
private void updateRaceStatusEnum() { private void updateRaceStatusEnum() {
//The amount of milliseconds until the race starts. //The millisecond duration of the race. Negative means it hasn't started, so we flip sign.
long timeToStart = this.raceClock.getDurationMilli(); long timeToStart = - this.raceClock.getDurationMilli();
if (timeToStart > Constants.RacePreStartTime) { if (timeToStart > Constants.RacePreStartTime) {
@ -279,7 +281,7 @@ public class MockRace extends Race {
boat.getSourceID(), boat.getSourceID(),
boat.getStatus(), boat.getStatus(),
boat.getCurrentLeg().getLegNumber(), boat.getCurrentLeg().getLegNumber(),
boat.getEstimatedTime() ); boat.getEstimatedTimeAtNextMark().toInstant().toEpochMilli() );
boatStatuses.add(boatStatus); 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. * Countdown timer until race starts.
*/ */
@ -335,6 +349,9 @@ public class MockRace extends Race {
//Update the race status based on the current time. //Update the race status based on the current time.
updateRaceStatusEnum(); updateRaceStatusEnum();
//Provide boat's with an estimated time at next mark until the race starts.
setBoatsTimeNextMark(raceClock.getCurrentTime());
//Parse the boat locations. //Parse the boat locations.
parseBoatLocations(); parseBoatLocations();
@ -910,8 +927,13 @@ public class MockRace extends Race {
double velocityToMark = boat.getCurrentSpeed() * cos(boat.getBearing().radians() - boat.calculateBearingToNextMarker().radians()) / Constants.KnotsToMMPerSecond; double velocityToMark = boat.getCurrentSpeed() * cos(boat.getBearing().radians() - boat.calculateBearingToNextMarker().radians()) / Constants.KnotsToMMPerSecond;
if (velocityToMark > 0) { if (velocityToMark > 0) {
//Calculate milliseconds until boat reaches mark.
long timeFromNow = (long) (1000 * boat.calculateDistanceToNextMarker() / velocityToMark); 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. * @param markRounding The MarkRounding message to set.
*/ */
public void setMarkRounding(MarkRounding markRounding) { 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); markRoundingMap.put(markRounding.getSourceID(), markRounding);
} }
@ -221,6 +221,7 @@ public class LatestMessages extends Observable {
public void setRaceXMLMessage(XMLMessage raceXMLMessage) { public void setRaceXMLMessage(XMLMessage raceXMLMessage) {
this.raceXMLMessage = raceXMLMessage; this.raceXMLMessage = raceXMLMessage;
this.setChanged();
this.notifyObservers(); this.notifyObservers();
} }
@ -240,6 +241,7 @@ public class LatestMessages extends Observable {
public void setBoatXMLMessage(XMLMessage boatXMLMessage) { public void setBoatXMLMessage(XMLMessage boatXMLMessage) {
this.boatXMLMessage = boatXMLMessage; this.boatXMLMessage = boatXMLMessage;
this.setChanged();
this.notifyObservers(); this.notifyObservers();
} }
@ -259,6 +261,7 @@ public class LatestMessages extends Observable {
public void setRegattaXMLMessage(XMLMessage regattaXMLMessage) { public void setRegattaXMLMessage(XMLMessage regattaXMLMessage) {
this.regattaXMLMessage = regattaXMLMessage; this.regattaXMLMessage = regattaXMLMessage;
this.setChanged();
this.notifyObservers(); this.notifyObservers();
} }

@ -31,12 +31,12 @@ public class BoatXMLReader extends XMLReader implements BoatDataSource {
/** /**
* Constructor for Boat XML using a file read as a resource. * 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 XMLReaderException Thrown if the file cannot be parsed.
* @throws InvalidBoatDataException Thrown if the file cannot be parsed correctly. * @throws InvalidBoatDataException Thrown if the file cannot be parsed correctly.
*/ */
public BoatXMLReader(String filePath) throws XMLReaderException, InvalidBoatDataException { public BoatXMLReader(String fileContents) throws XMLReaderException, InvalidBoatDataException {
super(filePath); super(fileContents);
//Attempt to read boat xml file. //Attempt to read boat xml file.
try { try {

@ -82,13 +82,13 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
/** /**
* Constructor for Streamed Race XML * 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 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. * @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. //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. * 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(); readRace();
readParticipants(); readParticipants();
readCourse(); readCourse();
@ -127,7 +128,7 @@ public class RaceXMLReader extends XMLReader implements RaceDataSource {
//Race type. //Race type.
String raceTypeString = getTextValueOfNode(settings, "RaceType"); String raceTypeString = getTextValueOfNode(settings, "RaceType");
raceType = RaceTypeEnum.valueOf(raceTypeString); raceType = RaceTypeEnum.fromString(raceTypeString);
//XML creation time. //XML creation time.
creationTimeDate = ZonedDateTime.parse(getTextValueOfNode(settings, "CreationTimeDate"), dateFormat); 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. * 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(); readCompoundMarks();
readCompoundMarkSequence(); readCompoundMarkSequence();
readCourseLimits(); 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. * 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 * @see CompoundMark
*/ */
private void readCompoundMarks() { private void readCompoundMarks() throws InvalidRaceDataException {
//Gets the "<Course>...</..>" element. //Gets the "<Course>...</..>" element.
Element course = (Element) doc.getElementsByTagName("Course").item(0); 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. //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 each CompoundMark element, create a CompoundMark object.
for(int i = 0; i < numberOfCompoundMarks; i++) { for(int i = 0; i < numberOfCompoundMarks; i++) {
//Get the CompoundMark element. //Get the CompoundMark element.
Element compoundMarkElement = (Element) course.getChildNodes().item(i); Element compoundMarkElement = (Element) compoundMarkList.item(i);
//If it is actually a CompoundMark element, turn it into a CompoundMark object.
if(compoundMarkElement.getNodeName().equals("CompoundMark")) {
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"); NodeList corners = compoundMarkSequence.getElementsByTagName("Corner");
//Gets the first 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. //Gets the ID number of this corner element.
int cornerID = getCompoundMarkID(cornerElement); 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. //The "<CourseLimit>...</...>" element. This contains a sequence of Limit elements.
Element courseLimit = (Element) doc.getElementsByTagName("CourseLimit").item(0); Element courseLimit = (Element) doc.getElementsByTagName("CourseLimit").item(0);
//Get the list of Limit elements.
NodeList limitList = courseLimit.getElementsByTagName("Limit");
//For each limit element... //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. //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. //Convert to GPSCoordinate.
if (limit.getNodeName().equals("Limit")) { double latitude = Double.parseDouble(limit.getAttribute("Lat"));
double longitude = Double.parseDouble(limit.getAttribute("Lon"));
double latitude = Double.parseDouble(limit.getAttribute("Lat")); boundary.add(new GPSCoordinate(latitude, longitude));
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. * 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 XMLReaderException Thrown if the file cannot be parsed.
* @throws InvalidRegattaDataException Thrown if the file cannot be parsed correctly. * @throws InvalidRegattaDataException Thrown if the file cannot be parsed correctly.
*/ */
public RegattaXMLReader(String filePath) throws XMLReaderException, InvalidRegattaDataException { public RegattaXMLReader(String fileContents) throws XMLReaderException, InvalidRegattaDataException {
super(filePath); super(fileContents);
//Attempt to read boat xml file. //Attempt to read boat xml file.
try { try {

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

@ -1,10 +1,12 @@
package shared.model; package shared.model;
import javafx.beans.property.SimpleStringProperty; import com.sun.istack.internal.Nullable;
import javafx.beans.property.StringProperty; import javafx.beans.property.*;
import network.Messages.Enums.BoatStatusEnum; 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. * 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. * The current speed of the boat, in knots.
* TODO knots * TODO knots
*/ */
private double currentSpeed; private DoubleProperty currentSpeed = new SimpleDoubleProperty(0);
/** /**
* The current bearing/heading of the boat. * 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. * 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. * 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). * 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. * 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. * @return The current speed of the boat, in knots.
*/ */
public double getCurrentSpeed() { 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. * @param currentSpeed The new speed of the boat, in knots.
*/ */
public void setCurrentSpeed(double currentSpeed) { 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. * @return The current leg of the race the boat is in.
*/ */
public Leg getCurrentLeg() { 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; return currentLeg;
} }
@ -198,7 +222,7 @@ public class Boat {
* @param currentLeg The new leg of the race the boat is in. * @param currentLeg The new leg of the race the boat is in.
*/ */
public void setCurrentLeg(Leg currentLeg) { public void setCurrentLeg(Leg currentLeg) {
this.currentLeg = currentLeg; this.currentLeg.setValue(currentLeg);
this.setTimeElapsedInCurrentLeg(0); this.setTimeElapsedInCurrentLeg(0);
this.setDistanceTravelledInLeg(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() { public StringProperty positionProperty() {
return positionInRace; 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) { public void setPosition(String position) {
this.positionInRace.set(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() { public String getPosition() {
return this.positionInRace.get(); 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. * 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. * 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. * The race pre-start time, in milliseconds. 3 minutes.
@ -49,9 +49,22 @@ public class Constants {
/** /**
* The number of milliseconds in one hour. * 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; 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) { 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; double shrinkDistance = 50d;
List<GPSCoordinate> shrunkBoundary = new ArrayList<>(boundary.size()); List<GPSCoordinate> shrunkBoundary = new ArrayList<>(boundary.size());

@ -1,5 +1,7 @@
package shared.model; package shared.model;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import network.Messages.Enums.RaceStatusEnum; import network.Messages.Enums.RaceStatusEnum;
import network.Messages.Enums.RaceTypeEnum; import network.Messages.Enums.RaceTypeEnum;
import network.Messages.LatestMessages; 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. * 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. * 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. * Returns the number of frames generated per second.
* @return Frames per second. * @return Frames per second.
*/ */
public int getFps() { 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 we have reached 1 second period, snapshot the framerate and reset.
if (this.lastFpsResetTime > 1000) { if (this.lastFpsResetTime > 1000) {
this.lastFps = this.currentFps; this.lastFps.set(this.currentFps);
this.currentFps = 0; this.currentFps = 0;
this.lastFpsResetTime = 0; this.lastFpsResetTime = 0;

@ -72,8 +72,9 @@ public class RaceClock implements Runnable {
/** /**
* Format string used for current time. * 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. * 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.TableColumn;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.layout.AnchorPane; import javafx.scene.layout.AnchorPane;
import seng302.Model.Boat; import visualiser.model.VisualiserBoat;
import java.net.URL; import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/** /**
* Finish Screen for when the race finishs. * Finish Screen for when the race finishes.
*/ */
public class FinishController extends Controller { public class FinishController extends Controller {
@ -22,41 +22,67 @@ public class FinishController extends Controller {
AnchorPane finishWrapper; AnchorPane finishWrapper;
@FXML @FXML
TableView<Boat> boatInfoTable; TableView<VisualiserBoat> boatInfoTable;
@FXML @FXML
TableColumn<Boat, String> boatRankColumn; TableColumn<VisualiserBoat, String> boatRankColumn;
@FXML @FXML
TableColumn<Boat, String> boatNameColumn; TableColumn<VisualiserBoat, String> boatNameColumn;
@FXML @FXML
Label raceWinnerLabel; 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 * Sets up the finish table
* @param boats Boats to display * @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); boatInfoTable.setItems(boats);
boatNameColumn.setCellValueFactory(cellData -> cellData.getValue().getName());
//Name.
boatNameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
//Rank/position.
boatRankColumn.setCellValueFactory(cellData -> cellData.getValue().positionProperty()); boatRankColumn.setCellValueFactory(cellData -> cellData.getValue().positionProperty());
//Winner label.
raceWinnerLabel.setText("Winner: "+ boatNameColumn.getCellObservableValue(0).getValue()); raceWinnerLabel.setText("Winner: "+ boatNameColumn.getCellObservableValue(0).getValue());
raceWinnerLabel.setWrapText(true); raceWinnerLabel.setWrapText(true);
} }
@Override
public void initialize(URL location, ResourceBundle resources){
}
/** /**
* Display the table * Display the table
* @param boats boats to display on 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); finishWrapper.setVisible(true);
setFinishTable(boats); 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. * Controller that everything is overlayed onto. This makes it so that changing scenes does not resize your stage.
*/ */
public class MainController extends Controller { public class MainController extends Controller {
@FXML private StartController startController; @FXML private StartController startController;
@FXML private RaceController raceController; @FXML private RaceController raceController;
@FXML private ConnectionController connectionController; @FXML private ConnectionController connectionController;
@FXML private FinishController finishController; @FXML private FinishController finishController;
/**
* Ctor.
*/
public MainController() {
}
/** /**
* Transitions from the StartController screen (displays pre-race information) to the RaceController (displays the actual race). * Transitions from the StartController screen (displays pre-race information) to the RaceController (displays the actual race).
* @param visualiserInput The object used to read packets from the race server. * @param visualiserInput The object used to read packets from the race server.
@ -29,10 +40,18 @@ public class MainController extends Controller {
raceController.startRace(visualiserInput, visualiserRace); 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) { public void enterLobby(Socket socket) {
startController.enterLobby(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) { public void enterFinish(ObservableList<VisualiserBoat> boats) {
finishController.enterFinish(boats); finishController.enterFinish(boats);
} }

@ -1,7 +1,9 @@
package visualiser.Controllers; package visualiser.Controllers;
import javafx.animation.AnimationTimer;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.chart.LineChart; import javafx.scene.chart.LineChart;
@ -10,13 +12,14 @@ import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import network.Messages.Enums.RaceStatusEnum;
import shared.model.Leg;
import visualiser.app.VisualiserInput; import visualiser.app.VisualiserInput;
import shared.model.RaceClock; import visualiser.model.*;
import visualiser.model.Sparkline;
import visualiser.model.VisualiserBoat;
import visualiser.model.VisualiserRace;
import java.net.URL; import java.net.URL;
import java.text.DecimalFormat;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/** /**
@ -34,8 +37,15 @@ public class RaceController extends Controller {
*/ */
private VisualiserRace visualiserRace; 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. * The sparkline graph.
@ -54,12 +64,13 @@ public class RaceController extends Controller {
@FXML private TableView<VisualiserBoat> boatInfoTable; @FXML private TableView<VisualiserBoat> boatInfoTable;
@FXML private TableColumn<VisualiserBoat, String> boatPlacingColumn; @FXML private TableColumn<VisualiserBoat, String> boatPlacingColumn;
@FXML private TableColumn<VisualiserBoat, String> boatTeamColumn; @FXML private TableColumn<VisualiserBoat, String> boatTeamColumn;
@FXML private TableColumn<VisualiserBoat, String> boatMarkColumn; @FXML private TableColumn<VisualiserBoat, Leg> boatMarkColumn;
@FXML private TableColumn<VisualiserBoat, String> boatSpeedColumn; @FXML private TableColumn<VisualiserBoat, Number> boatSpeedColumn;
@FXML private LineChart<Number, Number> sparklineChart; @FXML private LineChart<Number, Number> sparklineChart;
@FXML private AnchorPane annotationPane; @FXML private AnchorPane annotationPane;
/** /**
* Ctor. * Ctor.
*/ */
@ -68,10 +79,54 @@ public class RaceController extends Controller {
@Override @Override
public void initialize(URL location, ResourceBundle resources) { 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. * Initialises a listener for the fps toggle.
*/ */
@ -90,149 +145,240 @@ public class RaceController extends Controller {
} }
/** /**
* Updates the ResizableRaceCanvas (raceMap) with most recent data * Initialises the fps label to update when the race fps changes.
* * @param visualiserRace The race to monitor the frame rate of.
* @param boats boats that are to be displayed in the race
* @param boatMarkers Markers for boats
* @see ResizableRaceCanvas
*/ */
public void updateMap(ObservableList<Boat> boats, ObservableList<Marker> boatMarkers) { private void initialiseFpsLabel(VisualiserRace visualiserRace) {
raceMap.setBoats(boats);
raceMap.setBoatMarkers(boatMarkers); visualiserRace.fpsProperty().addListener((observable, oldValue, newValue) -> {
raceMap.update(); Platform.runLater(() -> this.FPS.setText("FPS: " + newValue.toString()));
raceBoundaries.draw(); });
//stop if the visualiser is no longer running
} }
/** /**
* 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. * @param race Race to listen to.
*/ */
public void setInfoTable(VisualiserRace race) { public void initialiseInfoTable(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());
}
//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. * Initialises the {@link Sparkline}, and listens to a specified {@link VisualiserRace}.
* @param boatsInRace used for current boat positions. * @param race The race to listen to.
*/ */
public void updateSparkline(ObservableList<VisualiserBoat> boatsInRace){ private void initialiseSparkline(VisualiserRace race) {
sparkline.updateSparkline(boatsInRace); this.sparkline = new Sparkline(race.getBoats(), race.getLegCount(), this.sparklineChart);
} }
/** /**
* Displays a specified race. * Initialises the {@link ResizableRaceCanvas}, provides the race to read data from.
* @param visualiserInput Object used to read packets from server. * @param race Race to read data from.
* @param visualiserRace Object modelling the race.
*/ */
public void startRace(VisualiserInput visualiserInput, VisualiserRace visualiserRace) { private void initialiseRaceCanvas(VisualiserRace race) {
this.visualiserInput = visualiserInput; //Create canvas.
this.visualiserRace = visualiserRace; 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()); //Add to scene.
raceMap.setMouseTransparent(true); canvasBase.getChildren().add(0, raceCanvas);
raceMap.widthProperty().bind(canvasBase.widthProperty());
raceMap.heightProperty().bind(canvasBase.heightProperty());
raceMap.draw();
raceMap.setVisible(true);
raceMap.setArrow(arrow.getChildren().get(0));
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.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(() -> { Platform.runLater(() -> {
timer.setText(newValue); 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 initialiseRace();
new Annotations(annotationPane, raceMap);
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. * @param boats boats there are in the race.
*/ */
public void finishRace(ObservableList<Boat> boats){ public void finishRace(ObservableList<VisualiserBoat> boats){
race.setVisible(false); race.setVisible(false);
parent.enterFinish(boats); parent.enterFinish(boats);
} }
/** /**
* Set the value for the fps label * Adds the included arrow pane (see arrow.fxml) to the arrowPane (see race.fxml).
*
* @param fps fps that the label will be updated to
*/ */
public void setFrames(String fps) { private void initialiseArrow() {
FPS.setText((fps)); arrowPane.getChildren().add(arrow);
} }
/** /**
* Set up FPS display at bottom of screen * Timer which monitors the race.
*/ */
private void initializeFPS() { private void raceTimer() {
showFPS.setVisible(true); new AnimationTimer() {
showFPS.selectedProperty().addListener((ov, old_val, new_val) -> { @Override
if (showFPS.isSelected()) { public void handle(long arg0) {
FPS.setVisible(true);
} else {
FPS.setVisible(false);
}
});
}
private void makeArrow() { //Get the current race status.
arrowPane.getChildren().add(arrow); 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() { } else {
return raceClock; //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. //Create race.
this.visualiserRace = new VisualiserRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, this.colors); this.visualiserRace = new VisualiserRace(boatDataSource, raceDataSource, regattaDataSource, latestMessages, this.colors);
new Thread(this.visualiserRace).start();
//Initialise the boat table. //Initialise the boat table.
@ -193,6 +194,8 @@ public class StartController extends Controller implements Observer {
*/ */
private void initialiseRaceClockStartTime(VisualiserRace visualiserRace) { private void initialiseRaceClockStartTime(VisualiserRace visualiserRace) {
raceStartLabel.setText(visualiserRace.getRaceClock().getStartingTimeString());
visualiserRace.getRaceClock().startingTimeProperty().addListener((observable, oldValue, newValue) -> { visualiserRace.getRaceClock().startingTimeProperty().addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> { Platform.runLater(() -> {
raceStartLabel.setText(newValue); raceStartLabel.setText(newValue);

@ -28,7 +28,7 @@ public class App extends Application {
System.exit(0); 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(); Parent root = loader.load();
Scene scene = new Scene(root, 1200, 800); Scene scene = new Scene(root, 1200, 800);
stage.setScene(scene); stage.setScene(scene);

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

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

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

@ -28,9 +28,10 @@ public class RaceConnection {
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public boolean check() { 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); InetSocketAddress i = new InetSocketAddress(hostname.get(), port);
try (Socket s = new Socket()){ 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"); status.set("Ready");
return true; return true;
} catch (IOException e) {} } catch (IOException e) {}

@ -1,40 +1,75 @@
package visualiser.model; package visualiser.model;
import shared.model.GPSCoordinate;
/** /**
* The base size of the map to be used for the * The base size of the map to be used for the
* {@link seng302.Model.ResizableRaceMap ResizableRaceMap} and * {@link ResizableRaceMap} and
* {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas}. It is used * {@link ResizableRaceCanvas}. It is used
* to convert {@link seng302.GPSCoordinate GPSCoordinate}s to relative * to convert {@link shared.model.GPSCoordinate}s to relative
* {@link seng302.GraphCoordinate GraphCoordinate}s. * {@link GraphCoordinate}s.
*/ */
public class RaceMap { public class RaceMap {
private final double x1;
private final double x2; /**
private final double y1; * The longitude of the left side of the map.
private final double y2; */
private int width, height; 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. * Constructor Method.
* *
* @param x1 Longitude of the top left point. * @param latTop Latitude of the top left point.
* @param y1 Latitude of the top left point. * @param longLeft Longitude of the top left point.
* @param x2 Longitude of the top right point. * @param latBottom Latitude of the top right point.
* @param y2 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 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. * @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) { public RaceMap(double latTop, double longLeft, double latBottom, double longRight, int width, int height) {
this.x1 = x1;
this.x2 = x2; //Long/lat edges.
this.y1 = y1; this.longLeft = longLeft;
this.y2 = y2; this.longRight = longRight;
this.latTop = latTop;
this.latBottom = latBottom;
//Pixel sizes.
this.width = width; this.width = width;
this.height = height; 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 lat GPS latitude
* @param lon GPS longitude * @param lon GPS longitude
@ -42,20 +77,50 @@ public class RaceMap {
* @see GraphCoordinate * @see GraphCoordinate
*/ */
private GraphCoordinate convertGPS(double lat, double lon) { 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) { if (width > height) {
size = height; x += extraPixels / 2;
return new GraphCoordinate((int) ((size * (lon - x1) / (x2 - x1)) + difference / 2), (int) (size - (size * (lat - y1) / (y2 - y1))));
} else { } 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. * @param coordinate GPSCoordinate representation of Latitude and Longitude.
* @return GraphCoordinate that the GPS is coordinates are to be displayed on the map. * @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()); 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) { public void setWidth(int width) {
this.width = 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) { public void setHeight(int height) {
this.height = height; this.height = height;
} }

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

@ -1,107 +1,161 @@
package visualiser.model; package visualiser.model;
import javafx.collections.ObservableList;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.paint.Paint; import javafx.scene.paint.Paint;
import javafx.scene.transform.Rotate; import javafx.scene.transform.Rotate;
import seng302.Mock.StreamedCourse; import network.Messages.Enums.BoatStatusEnum;
import seng302.RaceDataSource; import shared.dataInput.RaceDataSource;
import seng302.RaceMap; import shared.model.GPSCoordinate;
import shared.model.Mark;
import shared.model.RaceClock; import shared.model.RaceClock;
import java.time.Duration; import java.time.Duration;
import java.time.ZonedDateTime; import java.util.List;
import java.util.*;
/** /**
* This JavaFX Canvas is used to update and display details for a * This JavaFX Canvas is used to update and display details for a
* {@link seng302.RaceMap RaceMap} via the * {@link RaceMap} via the
* {@link seng302.Controllers.RaceController RaceController}.<br> * {@link visualiser.Controllers.RaceController}.<br>
* It fills it's parent and cannot be downsized. <br> * It fills it's parent and cannot be downsized. <br>
* Details displayed include: * Details displayed include:
* {@link seng302.Model.Boat Boats} (and their * {@link VisualiserBoat}s (and their
* {@link seng302.Model.TrackPoint TrackPoint}s), * {@link TrackPoint}s),
* {@link seng302.Model.Marker Markers}, a * {@link shared.model.Mark}s, a
* {@link seng302.Model.RaceClock RaceClock}, a wind direction arrow and * {@link RaceClock}, a wind direction arrow and
* various user selected {@link seng302.Model.Annotations Annotations}. * various user selected {@link Annotations}.
*/ */
public class ResizableRaceCanvas extends ResizableCanvas { public class ResizableRaceCanvas extends ResizableCanvas {
/**
* The RaceMap used for converting GPSCoordinates to GraphCoordinates.
*/
private RaceMap map; 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 annoName = true;
private boolean annoAbbrev = true; private boolean annoAbbrev = true;
private boolean annoSpeed = true; private boolean annoSpeed = true;
private boolean annoPath = true; private boolean annoPath = true;
private boolean annoEstTime = true; private boolean annoEstTime = true;
private boolean annoTimeSinceLastMark = 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 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(); super();
this.visualiserRace = visualiserRace;
this.arrow = arrow;
RaceDataSource raceData = visualiserRace.getRaceDataSource();
double lat1 = raceData.getMapTopLeft().getLatitude(); double lat1 = raceData.getMapTopLeft().getLatitude();
double long1 = raceData.getMapTopLeft().getLongitude(); double long1 = raceData.getMapTopLeft().getLongitude();
double lat2 = raceData.getMapBottomRight().getLatitude(); double lat2 = raceData.getMapBottomRight().getLatitude();
double long2 = raceData.getMapBottomRight().getLongitude(); 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. * Toggle name display in annotation
* */
* @param boats in race public void toggleAnnoName() {
annoName = !annoName;
}
/**
* Toggle boat path display in annotation
*/ */
public void setBoats(List<Boat> boats) { public void toggleBoatPath() {
this.boats = boats; annoPath = !annoPath;
mapBoatColours();
} }
public void toggleAnnoEstTime() {
annoEstTime = !annoEstTime;
}
/** /**
* Sets the boat markers that are to be displayed in this race. * Toggle boat time display in annotation
* */
* @param boatMarkers in race 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) { public void toggleAnnoSpeed() {
this.boatMarkers = boatMarkers; 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) { private void rotate(double degrees, double px, double py) {
this.map = map; 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()); * Draws a circle with a given diameter, centred on a given graph coordinate.
gc.fillPolygon(x, y, 3); * @param center The center coordinate of the circle.
gc.restore(); * @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 graphCoordinateA Starting Point of the line in GraphCoordinate.
* @param graphCoordinateB End Point of the line in GraphCoordinate. * @param graphCoordinateB End Point of the line in GraphCoordinate.
* @param paint Colour the line is to coloured. * @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.setStroke(paint);
gc.setFill(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;
/** //Draw first end-point.
* Displays an arrow representing wind direction on the Canvas drawCircle(graphCoordinateA, endPointDiameter);
*
* @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;
// show direction wind is coming from //Draw second end-point.
if (angle<180){angle = angle + 180;} drawCircle(graphCoordinateB, endPointDiameter);
else {angle = angle - 180;}
//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 graphCoordinate Coordinate that the point is to be displayed at.
* @param px Pivot point x of rotation. * @param paint Paint to use for the point.
* @param py Pivot point y of rotation.
*/ */
private void rotate(double angle, double px, double py) { private void drawPoint(GraphCoordinate graphCoordinate, Paint paint) {
Rotate r = new Rotate(angle, px, py);
gc.setTransform(r.getMxx(), r.getMyx(), r.getMxy(), r.getMyy(), r.getTx(), r.getTy()); //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 * 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 abbrev abbreviation of the boat name
* @param speed speed of the boat * @param speed speed of the boat
* @param coordinate coordinate the text appears * @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 = ""; String text = "";
//Check name toggle value
if (annoName){
//Draw name if annotation is enabled.
if (annoName) {
text += String.format("%s ", name); 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); text += String.format("%s ", abbrev);
} }
//Check speed toggle value
//Draw speed if annotation is enabled.
if (annoSpeed){ if (annoSpeed){
text += String.format("%.2fkn ", speed); text += String.format("%.2fkn ", speed);
} }
//Draw time to reach next mark if annotation is enabled.
if (annoEstTime) { if (annoEstTime) {
text += estTime; text += timeToNextMark;
} }
//Check time since last mark toggle value
if(annoTimeSinceLastMark){ //Draw time since last mark if annotation is enabled.
Duration timeSince = Duration.between(timeSinceLastMark, raceClock.getTime()); if(annoTimeSinceLastMark) {
text += String.format(" %ds ", timeSince.getSeconds()); text += timeSinceLastMark;
} }
//String text = String.format("%s, %2$.2fkn", name, speed);
//Offset by 20 pixels horizontally.
long xCoord = coordinate.getX() + 20; long xCoord = coordinate.getX() + 20;
long yCoord = coordinate.getY(); 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()) { if (xCoord + (text.length() * 7) >= getWidth()) {
xCoord -= text.length() * 7; xCoord -= text.length() * 7;
} }
if (yCoord - (text.length() * 2) <= 0) { if (yCoord - (text.length() * 2) <= 0) {
yCoord += 30; yCoord += 30;
} }
//Draw text.
gc.fillText(text, xCoord, yCoord); 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() { private void drawBoatText(VisualiserBoat boat) {
this.draw();
this.updateBoats(); 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() { private void drawBoats() {
for(Marker marker: markers) {
GraphCoordinate mark1 = this.map.convertGPS(marker.getMark1()); for (VisualiserBoat boat : visualiserRace.getBoats()) {
// removed drawing of lines between the marks as only
// the start and finish line should have a line drawn //Draw the boat.
if(marker.isCompoundMark()) { drawBoat(boat);
GraphCoordinate mark2 = this.map.convertGPS(marker.getMark2());
displayPoint(mark1, Color.LIMEGREEN); //Only draw wake if they are currently racing.
displayPoint(mark2, Color.LIMEGREEN); if (boat.getStatus() == BoatStatusEnum.RACING) {
} else { drawWake(boat);
displayPoint(mark1, Color.GREEN); }
//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(); //Convert position to graph coordinate.
double height = getHeight(); 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) { //The y coordinates of each vertex of the boat.
return;//TODO this should return a exception in the future double[] y = {
} pos.getY() + 12,
this.map.setHeight((int) height); pos.getY() - 12,
this.map.setWidth((int) width); 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() { private void drawWake(VisualiserBoat boat) {
annoName = !annoName;
//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() { private void displayWindArrow(double angle) {
annoPath = !annoPath;
}
public void toggleAnnoEstTime() { //We need to display wind-from, so add 180 degrees.
annoEstTime = !annoEstTime; 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() { private void drawMark(Mark mark) {
annoAbbrev = !annoAbbrev;
//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() { public void draw() {
annoSpeed = !annoSpeed;
//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() { private void clear() {
if (boats != null) { gc.clearRect(0, 0, getWidth(), getHeight());
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));
}
}
} }
/** /**
* Draws all track points for a given boat. Colour is set by boat, opacity by track point. * Draws the race boundary, and saves the image to {@link #background}.
* @param boat whose track is displayed * You should call {@link #clear()} before calling this.
* @param colour The color to use for the track.
* @see seng302.Model.TrackPoint
*/ */
private void drawTrack(Boat boat, Color colour) { private void redrawBoundaryImage() {
if (annoPath) {
for (TrackPoint point : boat.getTrack()) { //Prepare to draw.
GraphCoordinate scaledCoordinate = this.map.convertGPS(point.getCoordinate()); gc.setLineWidth(1);
gc.setFill(new Color(colour.getRed(), colour.getGreen(), colour.getBlue(), point.getAlpha())); gc.setFill(Color.AQUA);
gc.fillOval(scaledCoordinate.getX(), scaledCoordinate.getY(), 5, 5);
}
//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() { public void drawRace() {
colours = new ArrayList<>(Arrays.asList(
Color.BLUEVIOLET, gc.setLineWidth(2);
Color.BLACK,
Color.RED, clear();
Color.ORANGE,
Color.DARKOLIVEGREEN, //Race boundary.
Color.LIMEGREEN, drawBoundary();
Color.PURPLE,
Color.DARKGRAY, //Boats.
Color.YELLOW 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.chart.XYChart;
import javafx.scene.paint.Color; 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 * 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 * track of {@link VisualiserBoat}s in a race and their current
* placing position as they complete each {@link seng302.Model.Leg Leg} by * placing position as they complete each {@link shared.model.Leg} by
* passing a course {@link seng302.Model.Marker Marker}. <br> * passing a course {@link shared.model.Mark}. <br>
* This sparkline is displayed using the * This sparkline is displayed using the
* {@link seng302.Controllers.RaceController RaceController}. * {@link visualiser.Controllers.RaceController}.
*/ */
public class Sparkline { 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; private Integer legNum;
//TODO comment
private Integer sparkLineNumber = 0; private Integer sparkLineNumber = 0;
/**
* The linchart to plot sparklines on.
*/
@FXML LineChart<Number, Number> sparklineChart; @FXML LineChart<Number, Number> sparklineChart;
/**
* The x axis of the sparkline chart.
*/
@FXML NumberAxis xAxis; @FXML NumberAxis xAxis;
/**
* The y axis of the sparkline chart.
*/
@FXML NumberAxis yAxis; @FXML NumberAxis yAxis;
@ -37,17 +54,18 @@ public class Sparkline {
* @param legNum total number of legs in the race * @param legNum total number of legs in the race
* @param sparklineChart javaFX LineChart for the sparkline * @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) { LineChart<Number,Number> sparklineChart) {
this.boats = boats;
this.sparklineChart = sparklineChart; this.sparklineChart = sparklineChart;
this.legNum = legNum; this.legNum = legNum;
this.yAxis = (NumberAxis)sparklineChart.getYAxis(); this.yAxis = (NumberAxis) sparklineChart.getYAxis();
this.xAxis = (NumberAxis)sparklineChart.getXAxis(); this.xAxis = (NumberAxis) sparklineChart.getXAxis();
startBoats.addAll(boats);
makeColours();
mapBoatColours();
createSparkline(); 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 // NOTE: Y axis is in negatives to display correct positions
// all boats start in 'last' place // 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(); XYChart.Series<Number, Number> series = new XYChart.Series();
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, -startBoats.size())); series.getData().add(new XYChart.Data(0, -boats.size()));
sparklineChart.getData().add(series); sparklineChart.getData().add(series);
sparklineChart.getData().get(i).getNode().setStyle("-fx-stroke: " + sparklineChart.getData().get(i).getNode().setStyle("-fx-stroke: " +
""+boatColours.get(startBoats.get(i).getSourceID())+";"); "" + colourToHex(boats.get(i).getColor()) + ";");
} }
sparklineChart.setCreateSymbols(false); sparklineChart.setCreateSymbols(false);
@ -76,11 +94,11 @@ public class Sparkline {
xAxis.setTickMarkVisible(false); xAxis.setTickMarkVisible(false);
xAxis.setTickLabelsVisible(false); xAxis.setTickLabelsVisible(false);
xAxis.setMinorTickVisible(false); xAxis.setMinorTickVisible(false);
xAxis.setUpperBound((startBoats.size()+1)*legNum); xAxis.setUpperBound((boats.size()+1)*legNum);
xAxis.setTickUnit((startBoats.size()+1)*legNum); xAxis.setTickUnit((boats.size()+1)*legNum);
// set y axis details // set y axis details
yAxis.setLowerBound(-(startBoats.size()+1)); yAxis.setLowerBound(-(boats.size()+1));
yAxis.setUpperBound(0); yAxis.setUpperBound(0);
yAxis.setAutoRanging(false); yAxis.setAutoRanging(false);
yAxis.setLabel("Position in Race"); yAxis.setLabel("Position in Race");
@ -93,7 +111,7 @@ public class Sparkline {
@Override @Override
public String toString(Number value) { public String toString(Number value) {
if ((Double)value == 0.0 if ((Double)value == 0.0
|| (Double)value < -startBoats.size()){ || (Double)value < -boats.size()){
return ""; return "";
} }
else { else {
@ -103,6 +121,7 @@ public class Sparkline {
}); });
} }
/** /**
* Updates the sparkline to display current boat positions. * Updates the sparkline to display current boat positions.
* New points are plotted to represent each boat when required. * New points are plotted to represent each boat when required.
@ -113,8 +132,8 @@ public class Sparkline {
sparkLineNumber++; sparkLineNumber++;
for (int i = boatsInRace.size() - 1; i >= 0; i--){ for (int i = boatsInRace.size() - 1; i >= 0; i--){
for (int j = startBoats.size() - 1; j >= 0; j--){ for (int j = boats.size() - 1; j >= 0; j--){
if (boatsInRace.get(i)==startBoats.get(j)){ if (boatsInRace.get(i)== boats.get(j)){
// when a boat is on its first leg // when a boat is on its first leg
if (boatsInRace.get(i).getCurrentLeg().getLegNumber()==0){ 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) { private String colourToHex(Color color) {
return String.format( "#%02X%02X%02X", return String.format( "#%02X%02X%02X",
(int)( color.getRed() * 255 ), (int)( color.getRed() * 255 ),
@ -168,13 +179,4 @@ public class Sparkline {
(int)( color.getBlue() * 255 ) ); (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 * A TrackPoint is a point plotted to display the track a
* {@link VisualiserBoat Boat} has travelled in a race. <br> * {@link VisualiserBoat Boat} has travelled in a race. <br>
* TrackPoints are displayed on a * TrackPoints are displayed on a
* {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas}, via the * {@link ResizableRaceCanvas}, via the
* {@link seng302.Controllers.RaceController RaceController}. <br> * {@link visualiser.Controllers.RaceController}. <br>
* Track points can be made visible or hidden via the RaceController's * Track points can be made visible or hidden via the RaceController's
* {@link seng302.Model.Annotations Annotations}. * {@link Annotations}.
*/ */
public class TrackPoint { public class TrackPoint {
/**
* The {@link GPSCoordinate} this {@link TrackPoint} corresponds to.
*/
private final GPSCoordinate coordinate; private final GPSCoordinate coordinate;
/**
* The time the track point was created at, in milliseconds since unix epoch.
*/
private final long timeAdded; 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; private final long expiry;
/**
* The minimum alpha to draw the track point with.
*/
private final double minAlpha; 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. * 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.coordinate = coordinate;
this.timeAdded = timeAdded; this.timeAdded = timeAdded;
this.expiry = expiry; this.expiry = expiry;
this.minAlpha = 0.1; this.minAlpha = 0.1;
this.diameter = 5d;
} }
/** /**
* Gets the position of the point on physical race map. * 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. * 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() { 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() { public long getTimeAdded() {
return timeAdded; 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; package visualiser.model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import network.Messages.Enums.BoatStatusEnum; import network.Messages.Enums.BoatStatusEnum;
import org.geotools.referencing.GeodeticCalculator; import shared.model.Azimuth;
import shared.model.Boat; import shared.model.Boat;
import shared.model.Constants;
import shared.model.GPSCoordinate; import shared.model.GPSCoordinate;
import java.awt.geom.Point2D; import java.time.Duration;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Queue; import java.util.ArrayList;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.List;
/** /**
* Represents a Boat on the visualiser side of a race. * 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. * 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 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. * The boat's color.
*/ */
private Color color; private Color color;
/** /**
* Boat initializer which keeps all of the information of the boat. * Scalar used to scale the boat's wake.
*
* @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.
*/ */
public VisualiserBoat(int sourceID, String name, String abbrev, Color color) { private static final double wakeScale = 5;
super(sourceID, name, abbrev);
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 boat The boat to convert into a MockBoat.
* @param color The color of the boat. * @param color The color of the boat.
@ -73,41 +79,62 @@ public class VisualiserBoat extends Boat {
* @return GPSCoordinate of wake endpoint. * @return GPSCoordinate of wake endpoint.
*/ */
public GPSCoordinate getWake() { public GPSCoordinate getWake() {
double reverseHeading = getBearing().degrees() - 180;
double wakeScale = 5;
double distance = wakeScale * getCurrentSpeed(); //Calculate the reverse bearing of the boat, and convert it to an azimuth.
Azimuth reverseAzimuth = Azimuth.fromDegrees(getBearing().degrees() - 180d);
GeodeticCalculator calc = new GeodeticCalculator();
calc.setStartingGeographicPoint( //Calculate the distance, in meters, of the wake.
new Point2D.Double(getCurrentPosition().getLongitude(), getCurrentPosition().getLatitude()) //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();
calc.setDirection(reverseHeading, distance); double speedMetersPerHour = speedKnots * Constants.NMToMetersConversion;
Point2D endpoint = calc.getDestinationGeographicPoint(); double speedMetersPerSecond = speedMetersPerHour / Constants.OneHourSeconds;
return new GPSCoordinate(endpoint.getY(), endpoint.getX());
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. * Attempts to add a new point to boat's track.
* @param coordinate of point on 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 * @see TrackPoint
*/ */
public void addTrackPoint(GPSCoordinate coordinate) { public void addTrackPoint(GPSCoordinate coordinate) {
Boolean added = System.currentTimeMillis() >= nextValidTime;
//Get current time.
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
if (added && (this.getStatus() == BoatStatusEnum.RACING)) {
float trackPointTimeInterval = 5000; //Check if enough time has passed to create a new track point.
nextValidTime = currentTime + (long) trackPointTimeInterval; Boolean canBeAdded = currentTime >= nextValidTime;
int TRACK_POINT_LIMIT = 10;
track.add(new TrackPoint(coordinate, currentTime, TRACK_POINT_LIMIT * (long) trackPointTimeInterval)); //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. * 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 * @see TrackPoint
*/ */
public Queue<TrackPoint> getTrack() { public List<TrackPoint> getTrack() {
return track; return track;
} }
@ -128,27 +155,59 @@ public class VisualiserBoat extends Boat {
return getName(); 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 " -"; return " -";
} }
if (getEstimatedTime() <= 60) {
return " " + getEstimatedTime() + "s";
if (secondsUntil <= 60) {
//If less than 1 minute, display seconds only.
return " " + secondsUntil + "s";
} else { } else {
long seconds = getEstimatedTime() % 60; //Otherwise display minutes and seconds.
long minutes = (getEstimatedTime() - seconds) / 60; long seconds = secondsUntil % 60;
long minutes = (secondsUntil - seconds) / 60;
return String.format(" %dm %ds", minutes, seconds); 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; package visualiser.model;
import javafx.animation.AnimationTimer; import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
@ -15,8 +14,9 @@ import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource; import shared.dataInput.RaceDataSource;
import shared.dataInput.RegattaDataSource; import shared.dataInput.RegattaDataSource;
import shared.model.*; import shared.model.*;
import visualiser.Controllers.RaceController;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; 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. * Generates a list of VisualiserBoats given a list of Boats, and a list of participating boats.
* @param boats The map of Boats describing boats that are potentially in the race. Maps boat sourceID to boat. * @param 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); visualiserBoats.add(visualiserBoat);
//Next color.
colorIndex++;
} }
return visualiserBoats; return visualiserBoats;
@ -139,7 +150,7 @@ public class VisualiserRace extends Race {
for (VisualiserBoat boat : boats) { for (VisualiserBoat boat : boats) {
boat.setCurrentLeg(startingLeg); 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())); boat.setBearing(Bearing.fromDegrees(boatLocation.getHeadingDegrees()));
//Time until next mark. //Time until next mark.
boat.setEstimatedTime(convertEstTime(boatStatus.getEstTimeAtNextMark(), boatLocation.getTime())); boat.setEstimatedTimeAtNextMark(raceClock.getLocalTime(boatStatus.getEstTimeAtNextMark()));
//Speed. //Speed.
boat.setCurrentSpeed(boatLocation.getBoatSOG() / Constants.KnotsToMMPerSecond); boat.setCurrentSpeed(boatLocation.getBoatSOG() / Constants.KnotsToMMPerSecond);
//Boat status. //Boat status.
BoatStatusEnum boatStatusEnum = BoatStatusEnum.fromByte(boatStatus.getBoatStatus()); BoatStatusEnum newBoatStatusEnum = BoatStatusEnum.fromByte(boatStatus.getBoatStatus());
boat.setStatus(boatStatusEnum);
//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. //Leg.
@ -200,18 +217,18 @@ public class VisualiserRace extends Race {
if (legNumber >= 1 && legNumber < legs.size()) { if (legNumber >= 1 && legNumber < legs.size()) {
if (boat.getCurrentLeg() != legs.get(legNumber)) { if (boat.getCurrentLeg() != legs.get(legNumber)) {
boat.setCurrentLeg(legs.get(legNumber)); boat.setCurrentLeg(legs.get(legNumber));
boat.setTimeSinceLastMark(this.raceClock.getCurrentTime()); boat.setTimeAtLastMark(this.raceClock.getCurrentTime());
} }
} }
//Add a track point. //Attempt to add a track point.
if (boatStatusEnum == BoatStatusEnum.RACING) { if (newBoatStatusEnum == BoatStatusEnum.RACING) {
boat.addTrackPoint(boat.getCurrentPosition()); boat.addTrackPoint(boat.getCurrentPosition());
} }
//Set finish time if boat finished. //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.setTimeFinished(boatLocation.getTime());
boat.setStatus(BoatStatusEnum.FINISHED); boat.setStatus(BoatStatusEnum.FINISHED);
@ -297,16 +314,14 @@ public class VisualiserRace extends Race {
new AnimationTimer() { new AnimationTimer() {
//final long timeRaceStarted = System.currentTimeMillis(); //start time of loop long lastFrameTime = System.currentTimeMillis();
int fps = 0; //init fps value
//long timeCurrent = System.currentTimeMillis(); //current time
@Override @Override
public void handle(long arg0) { public void handle(long arg0) {
//totalTimeElapsed = System.currentTimeMillis() - timeRaceStarted; //Calculate the frame period.
long currentFrameTime = System.currentTimeMillis();
long framePeriod = currentFrameTime - lastFrameTime;
//Update racing boats. //Update racing boats.
updateBoats(boats, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap()); updateBoats(boats, latestMessages.getBoatLocationMap(), latestMessages.getBoatStatusMap());
@ -325,6 +340,10 @@ public class VisualiserRace extends Race {
stop(); stop();
} }
lastFrameTime = currentFrameTime;
//Increment fps.
incrementFps(framePeriod);
} }
}.start(); }.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 they're on the same leg, we need to compare time to finish leg.
if (legNumberDelta == 0) { if (legNumberDelta == 0) {
return (int) (b.getEstimatedTime() - a.getEstimatedTime()); return (int) Duration.between(b.getEstimatedTimeAtNextMark(), a.getEstimatedTimeAtNextMark()).toMillis();
} else { } else {
return legNumberDelta; return legNumberDelta;
} }
@ -381,13 +400,18 @@ public class VisualiserRace extends Race {
* Takes an estimated time an event will occur, and converts it to the * Takes an estimated time an event will occur, and converts it to the
* number of seconds before the event will occur. * 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 * @return int difference between time the race started and the estimated time
*/ */
private int convertEstTime(long estTimeMillis, long currentTime) { private int convertEstTime(long estTimeMillis, long currentTime) {
//Calculate millisecond delta.
long estElapsedMillis = estTimeMillis - currentTime; long estElapsedMillis = estTimeMillis - currentTime;
int estElapsedSecs = Math.round(estElapsedMillis/1000);
//Convert milliseconds to seconds.
int estElapsedSecs = Math.round(estElapsedMillis / 1000);
return estElapsedSecs; return estElapsedSecs;
} }

@ -4,7 +4,7 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?> <?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> <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"> <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> <columnConstraints>

@ -3,7 +3,7 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?> <?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> <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"> <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> <columnConstraints>

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.AnchorPane?> <?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> <children>
<fx:include fx:id="race" source="race.fxml" /> <fx:include fx:id="race" source="race.fxml" />
<fx:include fx:id="start" source="start.fxml" /> <fx:include fx:id="start" source="start.fxml" />

@ -6,7 +6,7 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?> <?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> <items>
<GridPane fx:id="canvasBase"> <GridPane fx:id="canvasBase">
<columnConstraints> <columnConstraints>

@ -3,7 +3,7 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?> <?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> <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"> <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> <columnConstraints>
@ -43,4 +43,4 @@
</children> </children>
</GridPane> </GridPane>
</children> </children>
</AnchorPane> </AnchorPane>

Loading…
Cancel
Save