Refactored and moved RaceXMLReader to shared/dataInput.

RaceStatus can provide windspeed in knots in addition to mm/sec.
RaceDataSource now provides a list of participating boat sourceIDs instead of boats.
Added a RegattaDataSource interface.
Angle, Azimuth, Bearing and mutable - careful.
Boat has positionInRace.
CompoundMarks have an ID and name.
Marks can be moved (setPosition(...)).

Refactored Mock.Race, Visualiser.StreamedCourse, Visualiser.StreamedRace, into (shared) Race, MockRace, VisualiserRace.

VisualiserBoat has color.
Added xml and polar files into resources folder.
This commit is contained in:
fjc40
2017-07-06 01:30:55 +12:00
parent 7f027c8cc5
commit 8d36d89570
35 changed files with 1806 additions and 1221 deletions
@@ -10,10 +10,15 @@ import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import seng302.Mock.StreamedRace;
import seng302.VisualiserInput;
import javafx.scene.paint.Color;
import visualiser.model.RaceClock;
import visualiser.model.Sparkline;
import visualiser.model.VisualiserBoat;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ResourceBundle;
/**
@@ -89,7 +94,7 @@ public class RaceController extends Controller {
* Creates and sets initial display for Sparkline for race positions.
* @param boats boats to display on the sparkline
*/
public void createSparkLine(ObservableList<Boat> boats){
public void createSparkLine(ObservableList<VisualiserBoat> boats){
sparkline = new Sparkline(boats, legNum, sparklineChart);
}
@@ -97,7 +102,7 @@ public class RaceController extends Controller {
* Updates the sparkline to display current boat positions.
* @param boatsInRace used for current boat positions.
*/
public void updateSparkline(ObservableList<Boat> boatsInRace){
public void updateSparkline(ObservableList<VisualiserBoat> boatsInRace){
sparkline.updateSparkline(boatsInRace);
}
@@ -147,7 +152,19 @@ public class RaceController extends Controller {
this.raceClock = raceClock;
raceMap.setRaceClock(raceClock);
StreamedRace newRace = new StreamedRace(visualiserInput, this);
//TODO move this list of colors somewhere more sensible.
List<Color> colours = new ArrayList<>(Arrays.asList(
Color.BLUEVIOLET,
Color.BLACK,
Color.RED,
Color.ORANGE,
Color.DARKOLIVEGREEN,
Color.LIMEGREEN,
Color.PURPLE,
Color.DARKGRAY,
Color.YELLOW
));
StreamedRace newRace = new StreamedRace(visualiserInput, colours, this);
initializeFPS();
@@ -1,290 +0,0 @@
package visualiser.dataInput;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import seng302.GPSCoordinate;
import seng302.Model.Leg;
import seng302.Model.Marker;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* XML Read for the Course that is being received
*/
public class StreamedCourseXMLReader extends XMLReader {
private static final double COORDINATEPADDING = 0.000;
private GPSCoordinate mapTopLeft, mapBottomRight;
private final List<GPSCoordinate> boundary = new ArrayList<>();
private final Map<Integer,Element> compoundMarks = new HashMap<>();
private final Map<Integer, StreamedBoat> participants = new HashMap<>();
private final List<Leg> legs = new ArrayList<>();
private final List<Marker> markers = new ArrayList<>();
private ZonedDateTime creationTimeDate;
private ZonedDateTime raceStartTime;
private int raceID;
private String raceType;
private boolean postpone;
/**
* Constructor for Streamed Race XML
* @param filePath file path to read
* @param read whether or not to read and store the files straight away.
* @throws IOException error
* @throws SAXException error
* @throws ParserConfigurationException error
* @throws StreamedCourseXMLException error
*/
public StreamedCourseXMLReader(String filePath, boolean read) throws IOException, SAXException, ParserConfigurationException, StreamedCourseXMLException {
super(filePath);
if (read) {
read();
}
}
/**
* Constructor for Streamed Race XML
* @param xmlString string to read
* @throws IOException error
* @throws SAXException error
* @throws ParserConfigurationException error
* @throws StreamedCourseXMLException error
*/
public StreamedCourseXMLReader(InputStream xmlString) throws IOException, SAXException, ParserConfigurationException, StreamedCourseXMLException {
super(xmlString);
read();
}
/**
* reads
* @throws StreamedCourseXMLException error
*/
private void read() throws StreamedCourseXMLException {
readRace();
readParticipants();
readCourse();
}
/**
* reads a race
*/
private void readRace() {
DateTimeFormatter dateFormat = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
Element settings = (Element) doc.getElementsByTagName("Race").item(0);
NamedNodeMap raceTimeTag = doc.getElementsByTagName("RaceStartTime").item(0).getAttributes();
if (raceTimeTag.getNamedItem("Time") != null) dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
raceID = Integer.parseInt(getTextValueOfNode(settings, "RaceID"));
raceType = getTextValueOfNode(settings, "RaceType");
creationTimeDate = ZonedDateTime.parse(getTextValueOfNode(settings, "CreationTimeDate"), dateFormat);
if (raceTimeTag.getNamedItem("Time") != null) raceStartTime = ZonedDateTime.parse(raceTimeTag.getNamedItem("Time").getTextContent(), dateFormat);
else raceStartTime = ZonedDateTime.parse(raceTimeTag.getNamedItem("Start").getTextContent(), dateFormat);
postpone = Boolean.parseBoolean(raceTimeTag.getNamedItem("Postpone").getTextContent());
}
/**
* Reads the participants of the race.
*/
private void readParticipants() {
Element nParticipants = (Element) doc.getElementsByTagName("Participants").item(0);
nParticipants.getChildNodes().getLength();
for (int i = 0; i < nParticipants.getChildNodes().getLength(); i++) {
int sourceID;
Node yacht = nParticipants.getChildNodes().item(i);
if (yacht.getNodeName().equals("Yacht")) {
if (exists(yacht, "SourceID")) {
sourceID = Integer.parseInt(yacht.getAttributes().getNamedItem("SourceID").getTextContent());
participants.put(sourceID, new StreamedBoat(sourceID));
}
}
}
}
/**
* reads a course
* @throws StreamedCourseXMLException error
*/
private void readCourse() throws StreamedCourseXMLException {
readCompoundMarks();
readCompoundMarkSequence();
readCourseLimit();
}
/**
* Indexes CompoundMark elements by their ID for use in generating the course, and populates list of Markers.
* @throws StreamedCourseXMLException if a CompoundMark element contains an unhandled number of compoundMarks.
* @see seng302.Model.Marker
*/
private void readCompoundMarks() throws StreamedCourseXMLException {
Element nCourse = (Element) doc.getElementsByTagName("Course").item(0);
for(int i = 0; i < nCourse.getChildNodes().getLength(); i++) {
Node compoundMark = nCourse.getChildNodes().item(i);
if(compoundMark.getNodeName().equals("CompoundMark")) {
int compoundMarkID = getCompoundMarkID((Element) compoundMark);
compoundMarks.put(compoundMarkID, (Element)compoundMark);
markers.add(getMarker(compoundMarkID));
}
}
}
/**
* Generates a Marker from the CompoundMark element with given ID.
* @param compoundMarkID index of required CompoundMark element
* @return generated Marker
* @throws StreamedCourseXMLException if CompoundMark element contains unhandled number of compoundMarks
* @see seng302.Model.Marker
*/
private Marker getMarker(int compoundMarkID) throws StreamedCourseXMLException {
Element compoundMark = compoundMarks.get(compoundMarkID);
NodeList nMarks = compoundMark.getElementsByTagName("Mark");
Marker marker;
switch(nMarks.getLength()) {
case 1: marker = new Marker(getCoordinate((Element)nMarks.item(0)),getSourceId((Element)nMarks.item(0))); break;
case 2: marker = new Marker(getCoordinate((Element)nMarks.item(0)), getCoordinate((Element)nMarks.item(1)),
getSourceId((Element)nMarks.item(0)), getSourceId((Element)nMarks.item(1))); break;
default: throw new StreamedCourseXMLException();
}
return marker;
}
/**
* Extracts the GPS Coordinates from a XMl Element
* @param mark Element to Extract from
* @return the GPS coordinate that it reflects
*/
private GPSCoordinate getCoordinate(Element mark) {
double lat = Double.parseDouble(mark.getAttribute("TargetLat"));
double lon = Double.parseDouble(mark.getAttribute("TargetLng"));
return new GPSCoordinate(lat,lon);
}
/**
* Extracts the SourceID from a XML ELement
* @param mark Element to Extract from
* @return the Source ID of the Extracted Element.
*/
private int getSourceId(Element mark) {
String sourceId = mark.getAttribute("SourceID");
if (sourceId.isEmpty()){
return 0;
}
return Integer.parseInt(sourceId);
}
/**
* Reads "compoundMarkID" attribute of CompoundMark or Corner element
* @param element with "compoundMarkID" attribute
* @return value of "compoundMarkID" attribute
*/
private int getCompoundMarkID(Element element) {
return Integer.parseInt(element.getAttribute("CompoundMarkID"));
}
/**
* Reads "name" attribute of CompoundMark element with corresponding CompoundMarkID
* @param compoundMarkID unique ID for CompoundMark element
* @return value of "name" attribute
*/
private String getCompoundMarkName(int compoundMarkID) {
return compoundMarks.get(compoundMarkID).getAttribute("Name");
}
/**
* Populates list of legs given CompoundMarkSequence element and referenced CompoundMark elements.
* @throws StreamedCourseXMLException if markers cannot be resolved from CompoundMark
*/
private void readCompoundMarkSequence() throws StreamedCourseXMLException {
Element nCompoundMarkSequence = (Element) doc.getElementsByTagName("CompoundMarkSequence").item(0);
NodeList nCorners = nCompoundMarkSequence.getElementsByTagName("Corner");
Element markXML = (Element)nCorners.item(0);
Marker lastMarker = getMarker(getCompoundMarkID(markXML));
String legName = getCompoundMarkName(getCompoundMarkID(markXML));
for(int i = 1; i < nCorners.getLength(); i++) {
markXML = (Element)nCorners.item(i);
Marker currentMarker = getMarker(getCompoundMarkID(markXML));
legs.add(new Leg(legName, lastMarker, currentMarker, i-1));
lastMarker = currentMarker;
legName = getCompoundMarkName(getCompoundMarkID(markXML));
}
}
/**
* Reads the course boundary limitations of the course recieved.
*/
private void readCourseLimit() {
Element nCourseLimit = (Element) doc.getElementsByTagName("CourseLimit").item(0);
for(int i = 0; i < nCourseLimit.getChildNodes().getLength(); i++) {
Node limit = nCourseLimit.getChildNodes().item(i);
if (limit.getNodeName().equals("Limit")) {
double lat = Double.parseDouble(limit.getAttributes().getNamedItem("Lat").getTextContent());
double lon = Double.parseDouble(limit.getAttributes().getNamedItem("Lon").getTextContent());
boundary.add(new GPSCoordinate(lat, lon));
}
}
double maxLatitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude() + COORDINATEPADDING;
double maxLongitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude() + COORDINATEPADDING;
double minLatitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude() + COORDINATEPADDING;
double minLongitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude() + COORDINATEPADDING;
mapTopLeft = new GPSCoordinate(minLatitude, minLongitude);
mapBottomRight = new GPSCoordinate(maxLatitude, maxLongitude);
}
public List<GPSCoordinate> getBoundary() {
return boundary;
}
public GPSCoordinate getMapTopLeft() {
return mapTopLeft;
}
public GPSCoordinate getMapBottomRight() {
return mapBottomRight;
}
public List<Leg> getLegs() {
return legs;
}
public List<Marker> getMarkers() { return markers; }
public Double getPadding() {
return COORDINATEPADDING;
}
public ZonedDateTime getRaceStartTime() {
return raceStartTime;
}
public int getRaceID() {
return raceID;
}
public String getRaceType() {
return raceType;
}
public boolean isPostpone() {
return postpone;
}
public Map<Integer, StreamedBoat> getParticipants() {
return participants;
}
}
@@ -1,7 +0,0 @@
package visualiser.exceptions;
/**
* Created by cbt24 on 25/04/17.
*/
public class StreamedCourseXMLException extends Throwable {
}
@@ -108,7 +108,7 @@ public class Sparkline {
* New points are plotted to represent each boat when required.
* @param boatsInRace current position of the boats in race
*/
public void updateSparkline(ObservableList<Boat> boatsInRace){
public void updateSparkline(ObservableList<VisualiserBoat> boatsInRace){
int placingVal = boatsInRace.size();
sparkLineNumber++;
@@ -1,106 +0,0 @@
package visualiser.model;
import seng302.GPSCoordinate;
import seng302.Model.Boat;
import seng302.Model.Leg;
import seng302.Model.Marker;
import seng302.RaceDataSource;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Observable;
/**
* COurse that the is being received.
*/
public class StreamedCourse extends Observable implements RaceDataSource {
private StreamedCourseXMLReader streamedCourseXMLReader = null;
private BoatXMLReader boatXMLReader = null;
private RegattaXMLReader regattaXMLReader = null;
private double windDirection = 0;
public StreamedCourse() {}
/**
* Read and set the new XML that has been received.
* @param boatXMLReader new XMl of the boats.
*/
public void setBoatXMLReader(BoatXMLReader boatXMLReader) {
this.boatXMLReader = boatXMLReader;
if (streamedCourseXMLReader != null && boatXMLReader != null) {
this.boatXMLReader.setParticipants(streamedCourseXMLReader.getParticipants());
boatXMLReader.read();
}
setChanged();
notifyObservers();
}
public StreamedCourseXMLReader getStreamedCourseXMLReader() {
return streamedCourseXMLReader;
}
/**
* Read and sets the new Course that has been received
* @param streamedCourseXMLReader COurse XML that has been received
*/
public void setStreamedCourseXMLReader(StreamedCourseXMLReader streamedCourseXMLReader) {
this.streamedCourseXMLReader = streamedCourseXMLReader;
if (streamedCourseXMLReader != null && boatXMLReader != null) {
boatXMLReader.setParticipants(streamedCourseXMLReader.getParticipants());
boatXMLReader.read();
}
}
/**
* Reads and sets the new Regatta that has been received
* @param regattaXMLReader Regatta XMl that has been received.
*/
public void setRegattaXMLReader(RegattaXMLReader regattaXMLReader) {
this.regattaXMLReader = regattaXMLReader;
setChanged();
notifyObservers();
}
public void setWindDirection(double windDirection) {
this.windDirection = windDirection;
}
public double getWindDirection() {
return windDirection;
}
public boolean hasReadRegatta() { return regattaXMLReader != null; }
public boolean hasReadBoats() { return boatXMLReader != null; }
public boolean hasReadCourse() { return streamedCourseXMLReader != null; }
public String getRegattaName() { return regattaXMLReader.getRegattaName(); }
public List<Boat> getBoats() {
return boatXMLReader.getBoats();
}
public List<Leg> getLegs() {
return streamedCourseXMLReader.getLegs();
}
public List<Marker> getMarkers() { return streamedCourseXMLReader.getMarkers(); }
public List<GPSCoordinate> getBoundary() {
return streamedCourseXMLReader.getBoundary();
}
public ZonedDateTime getZonedDateTime() {
return streamedCourseXMLReader.getRaceStartTime();
}
public GPSCoordinate getMapTopLeft() {
return streamedCourseXMLReader.getMapTopLeft();
}
public GPSCoordinate getMapBottomRight() {
return streamedCourseXMLReader.getMapBottomRight();
}
}
@@ -1,285 +0,0 @@
package visualiser.model;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import seng302.Controllers.FinishController;
import seng302.Controllers.RaceController;
import seng302.GPSCoordinate;
import seng302.Model.Boat;
import seng302.Model.Leg;
import seng302.Model.Marker;
import seng302.Networking.Messages.BoatLocation;
import seng302.Networking.Messages.BoatStatus;
import seng302.Networking.Messages.Enums.BoatStatusEnum;
import seng302.VisualiserInput;
import java.util.List;
/**
* The Class used to view the race streamed.
*/
public class StreamedRace implements Runnable {
private final VisualiserInput visualiserInput;
private final ObservableList<Boat> startingBoats;
private final ObservableList<Marker> boatMarkers;
private final List<Leg> legs;
private RaceController controller;
protected FinishController finishController;
private int boatsFinished = 0;
private long totalTimeElapsed;
private int lastFPS = 20;
public StreamedRace(VisualiserInput visualiserInput, RaceController controller) {
StreamedCourse course = visualiserInput.getCourse();
this.startingBoats = FXCollections.observableArrayList(course.getBoats());
this.boatMarkers = FXCollections.observableArrayList(course.getMarkers());
this.legs = course.getLegs();
this.legs.add(new Leg("Finish", this.legs.size()));
this.controller = controller;
if (startingBoats != null && startingBoats.size() > 0) {
initialiseBoats();
}
this.visualiserInput = visualiserInput;
}
private void initialiseBoats() {
Leg officialStart = legs.get(0);
String name = officialStart.getName();
Marker endCompoundMark = officialStart.getEndMarker();
for (Boat boat : startingBoats) {
if (boat != null) {
Leg startLeg = new Leg(name, 0);
startLeg.setEndMarker(endCompoundMark);
boat.setCurrentLeg(startLeg, controller.getRaceClock());
boat.setTimeSinceLastMark(controller.getRaceClock().getTime());
}
}
}
/**
* Checks if the boat cannot finish the race
* @return True if boat cannot finish the race
*/
protected boolean doNotFinish() {
// DNF is no longer random and is now determined by a dnf packet
return false;
}
/**
* Checks the position of the boat.
*
* @param boat Boat that the position is to be updated for.
* @param timeElapsed Time that has elapse since the start of the the race.
*/
private void checkPosition(Boat boat, long timeElapsed) {
boolean legChanged = false;
StreamedCourse raceData = visualiserInput.getCourse();
BoatStatus boatStatusMessage = visualiserInput.getBoatStatusMap().get(boat.getSourceID());
if (boatStatusMessage != null) {
BoatStatusEnum boatStatusEnum = BoatStatusEnum.fromByte(boatStatusMessage.getBoatStatus());
int legNumber = boatStatusMessage.getLegNumber();
if (legNumber >= 1 && legNumber < legs.size()) {
if (boat.getCurrentLeg() != legs.get(legNumber)){
boat.setCurrentLeg(legs.get(legNumber), controller.getRaceClock());
legChanged = true;
}
}
if (boatStatusEnum == BoatStatusEnum.RACING) {
boat.addTrackPoint(boat.getCurrentPosition());
} else if (boatStatusEnum == BoatStatusEnum.DNF) {
boat.setDnf(true);
} else if (boatStatusEnum == BoatStatusEnum.FINISHED || legNumber == raceData.getLegs().size()) {
boatsFinished++;
boat.setTimeFinished(timeElapsed);
boat.setFinished(true);
}
}
if (legChanged) {
//Update the boat display table in the GUI to reflect the leg change
updatePositions();
controller.updateSparkline(startingBoats);
}
}
/**
* Updates the boat's gps coordinates
*
* @param boat to be updated
*/
private void updatePosition(Boat boat) {
int sourceID = boat.getSourceID();
BoatLocation boatLocation = visualiserInput.getBoatLocationMessage(sourceID);
BoatStatus boatStatus = visualiserInput.getBoatStatusMessage(sourceID);
if(boatLocation != null) {
double lat = boatLocation.getLatitudeDouble();
double lon = boatLocation.getLongitudeDouble();
boat.setCurrentPosition(new GPSCoordinate(lat, lon));
boat.setHeading(boatLocation.getHeadingDegrees());
boat.setEstTime(convertEstTime(boatStatus.getEstTimeAtNextMark(), boatLocation.getTime()));
double MMPS_TO_KN = 0.001944;
boat.setVelocity(boatLocation.getBoatSOG() * MMPS_TO_KN);
}
}
/**
* Updates the boat's gps coordinates
*
* @param mark to be updated
*/
private void updateMarker(Marker mark) {
int sourceID = mark.getSourceId1();
BoatLocation boatLocation1 = visualiserInput.getBoatLocationMessage(sourceID);
if(boatLocation1 != null) {
double lat = boatLocation1.getLatitudeDouble();
double lon = boatLocation1.getLongitudeDouble();
mark.setCurrentPosition1(new GPSCoordinate(lat, lon));
}
int sourceID2 = mark.getSourceId2();
BoatLocation boatLocation2 = visualiserInput.getBoatLocationMessage(sourceID2);
if(boatLocation2 != null) {
double lat = boatLocation2.getLatitudeDouble();
double lon = boatLocation2.getLongitudeDouble();
mark.setCurrentPosition2(new GPSCoordinate(lat, lon));
}
}
public void setController(RaceController controller) {
this.controller = controller;
}
/**
* Runnable for the thread.
*/
public void run() {
setControllerListeners();
Platform.runLater(() -> controller.createSparkLine(startingBoats));
initialiseBoats();
startRaceStream();
}
/**
* Update the calculated fps to the fps label
*
* @param fps The new calculated fps value
*/
private void updateFPS(int fps) {
Platform.runLater(() -> controller.setFrames("FPS: " + fps));
}
/**
* Starts the Race Simulation, playing the race start to finish with the timescale.
* This prints the boats participating, the order that the events occur in time order, and the respective information of the events.
*/
private void startRaceStream() {
System.setProperty("javafx.animation.fullspeed", "true");
new AnimationTimer() {
final long timeRaceStarted = System.currentTimeMillis(); //start time of loop
int fps = 0; //init fps value
long timeCurrent = System.currentTimeMillis(); //current time
@Override
public void handle(long arg0) {
totalTimeElapsed = System.currentTimeMillis() - timeRaceStarted;
//Check if the race has actually started.
if (visualiserInput.getRaceStatus().isStarted()) {
//Set all boats to started.
for (Boat boat : startingBoats) {
boat.setStarted(true);
}
}
for (Boat boat : startingBoats) {
if (boat != null && !boat.isFinished()) {
updatePosition(boat);
checkPosition(boat, totalTimeElapsed);
}
}
for (Marker mark: boatMarkers){
if (mark != null){
updateMarker(mark);
}
}
if (visualiserInput.getRaceStatus().isFinished()) {
controller.finishRace(startingBoats);
stop();
}
controller.updateMap(startingBoats, boatMarkers);
fps++;
if ((System.currentTimeMillis() - timeCurrent) > 1000) {
updateFPS(fps);
lastFPS = fps;
fps = 0;
timeCurrent = System.currentTimeMillis();
}
}
}.start();
}
/**
* Update position of boats in race, no position if on starting leg or DNF.
*/
private void updatePositions() {
FXCollections.sort(startingBoats, (a, b) -> b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber());
for(Boat boat: startingBoats) {
if(boat != null) {
boat.setPosition(Integer.toString(startingBoats.indexOf(boat) + 1));
if (boat.isDnf() || !boat.isStarted() || boat.getCurrentLeg().getLegNumber() < 0)
boat.setPosition("-");
}
}
}
/**
* Update call for the controller.
*/
private void setControllerListeners() {
if (controller != null) controller.setInfoTable(this);
}
/**
* Returns the boats that have started the race.
*
* @return ObservableList of Boat class that participated in the race.
* @see ObservableList
* @see Boat
*/
public ObservableList<Boat> getStartingBoats() {
return startingBoats;
}
/**
* Takes an estimated time an event will occur, and converts it to the
* number of seconds before the event will occur.
*
* @param estTimeMillis estimated time in milliseconds
* @return int difference between time the race started and the estimated time
*/
private int convertEstTime(long estTimeMillis, long currentTime) {
long estElapsedMillis = estTimeMillis - currentTime;
int estElapsedSecs = Math.round(estElapsedMillis/1000);
return estElapsedSecs;
}
}
@@ -2,6 +2,7 @@ package visualiser.model;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.paint.Color;
import network.Messages.Enums.BoatStatusEnum;
import org.geotools.referencing.GeodeticCalculator;
import shared.model.Boat;
@@ -17,28 +18,50 @@ import java.util.concurrent.ConcurrentLinkedQueue;
* This adds visualiser specific functionality to a boat.
* This class is used to represent and store information about a boat which may
* travel around in a race. It is displayed on the
* {@link seng302.Model.ResizableRaceCanvas ResizableRaceCanvas} via the
* {@link seng302.Controllers.RaceController RaceController}.
* {@link ResizableRaceCanvas ResizableRaceCanvas} via the
* {@link visualiser.Controllers.RaceController RaceController}.
*/
public class VisualiserBoat extends Boat {
/**
* The collection of trackpoints generated for the boat.
*/
private final Queue<TrackPoint> track = new ConcurrentLinkedQueue<>();
private long nextValidTime = 0;
private ZonedDateTime timeSinceLastMark;
/**
* The boat's color.
*/
private Color color;
/**
* Boat initializer which keeps all of the information of the boat.
*
* @param sourceID The source ID of the boat.
* @param name Name of the Boat.
* @param abbrev The team/country abbreviation of the boat.
* @param color The color of the boat.
*/
public VisualiserBoat(int sourceID, String name, String abbrev) {
public VisualiserBoat(int sourceID, String name, String abbrev, Color color) {
super(sourceID, name, abbrev);
this.color = color;
}
/**
* Constructs a mock boat object from a given boat and polars table.
*
* @param boat The boat to convert into a MockBoat.
* @param color The color of the boat.
*/
public VisualiserBoat(Boat boat, Color color) {
super(boat.getSourceID(), boat.getName(), boat.getCountry());
this.color = color;
}
@@ -66,7 +89,7 @@ public class VisualiserBoat extends Boat {
/**
* Adds a new point to boat's track.
* @param coordinate of point on track
* @see seng302.Model.TrackPoint
* @see TrackPoint
*/
public void addTrackPoint(GPSCoordinate coordinate) {
Boolean added = System.currentTimeMillis() >= nextValidTime;
@@ -82,12 +105,19 @@ public class VisualiserBoat extends Boat {
/**
* Returns the boat's sampled track between start of race and current time.
* @return queue of track points
* @see seng302.Model.TrackPoint
* @see TrackPoint
*/
public Queue<TrackPoint> getTrack() {
return track;
}
/**
* Returns the color of the boat.
* @return The color of the boat.
*/
public Color getColor() {
return color;
}
/**
* Print method prints the name of the boat
@@ -0,0 +1,398 @@
package visualiser.model;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.paint.Color;
import network.Messages.BoatLocation;
import network.Messages.BoatStatus;
import network.Messages.Enums.BoatStatusEnum;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.RaceStatus;
import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import shared.dataInput.RegattaDataSource;
import shared.model.*;
import visualiser.Controllers.FinishController;
import visualiser.Controllers.RaceController;
import visualiser.app.VisualiserInput;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* The Class used to view the race streamed.
* Has a course, boats, boundaries, etc...
* Observes LatestMessages and updates its state based on new messages.
*/
public class VisualiserRace extends Race {
//TODO replace with LatestMessages
private final VisualiserInput visualiserInput;
/**
* An observable list of boats in the race.
*/
private final ObservableList<VisualiserBoat> boats;
/**
* An observable list of marker boats in the race.
*/
private final ObservableList<Mark> boatMarkers;
//TODO remove these controller references once refactored
private RaceController controller;
protected FinishController finishController;
//TODO remove?
private int lastFPS = 20;
public VisualiserRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, List<Color> colors, VisualiserInput visualiserInput, RaceController controller) {
super(boatDataSource, raceDataSource, regattaDataSource);
this.boats = FXCollections.observableArrayList(this.generateVisualiserBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), colors));
this.boatMarkers = FXCollections.observableArrayList(boatDataSource.getMarkerBoats().values());
this.controller = controller;
this.visualiserInput = visualiserInput;
}
/**
* Generates a list of VisualiserBoats given a list of Boats, and a list of participating boats.
* @param boats The map of Boats describing boats that are potentially in the race. Maps boat sourceID to boat.
* @param sourceIDs The list of boat sourceIDs describing which specific boats are actually participating.
* @param colors The list of colors to be used for the boats.
* @return A list of MockBoats that are participating in the race.
*/
private List<VisualiserBoat> generateVisualiserBoats(Map<Integer, Boat> boats, List<Integer> sourceIDs, List<Color> colors) {
List<VisualiserBoat> visualiserBoats = new ArrayList<>(sourceIDs.size());
//For each sourceID participating...
int colorIndex = 0;
for (int sourceID : sourceIDs) {
//Get the boat associated with the sourceID.
Boat boat = boats.get(sourceID);
//Get a color for the boat.
Color color = colors.get(colorIndex);
//Construct a VisualiserBoat using the Boat and Polars.
VisualiserBoat visualiserBoat = new VisualiserBoat(boat, color);
visualiserBoats.add(visualiserBoat);
}
return visualiserBoats;
}
/**
* Initialise the boats in the race.
* This sets their current leg.
*/
@Override
protected void initialiseBoats() {
Leg startingLeg = legs.get(0);
for (VisualiserBoat boat : boats) {
boat.setCurrentLeg(startingLeg);
boat.setTimeSinceLastMark(controller.getRaceClock().getTime());
}
}
/**
* Updates all of the racing boats based on messages received.
* @param boats The list of racing boats.
* @param boatLocationMap A map between boat sourceIDs and BoatLocation messages.
* @param boatStatusMap A map between boat sourceIDs and BoatStatus messages.
*/
private void updateBoats(ObservableList<VisualiserBoat> boats, Map<Integer, BoatLocation> boatLocationMap, Map<Integer, BoatStatus> boatStatusMap) {
for (VisualiserBoat boat : boats) {
BoatLocation boatLocation = boatLocationMap.get(boat.getSourceID());
BoatStatus boatStatus = boatStatusMap.get(boat.getSourceID());
updateBoat(boat, boatLocation, boatStatus);
}
}
/**
* Updates an individual racing boat based on messages received.
* @param boat The boat to update.
* @param boatLocation The BoatLocation message to use.
* @param boatStatus The BoatStatus message to use.
*/
private void updateBoat(VisualiserBoat boat, BoatLocation boatLocation, BoatStatus boatStatus) {
if (boatLocation != null && boatStatus != null) {
//Get the new position.
double latitude = boatLocation.getLatitudeDouble();
double longitude = boatLocation.getLongitudeDouble();
GPSCoordinate gpsCoordinate = new GPSCoordinate(latitude, longitude);
boat.setCurrentPosition(gpsCoordinate);
//Bearing.
boat.setBearing(Bearing.fromDegrees(boatLocation.getHeadingDegrees()));
//Time until next mark.
boat.setEstimatedTime(convertEstTime(boatStatus.getEstTimeAtNextMark(), boatLocation.getTime()));
//Speed.
boat.setCurrentSpeed(boatLocation.getBoatSOG() / Constants.KnotsToMMPerSecond);
//Boat status.
BoatStatusEnum boatStatusEnum = BoatStatusEnum.fromByte(boatStatus.getBoatStatus());
boat.setStatus(boatStatusEnum);
//Leg.
int legNumber = boatStatus.getLegNumber();
if (legNumber >= 1 && legNumber < legs.size()) {
if (boat.getCurrentLeg() != legs.get(legNumber)) {
boat.setCurrentLeg(legs.get(legNumber));
boat.setTimeSinceLastMark(controller.getRaceClock().getTime());
}
}
//Add a track point.
if (boatStatusEnum == BoatStatusEnum.RACING) {
boat.addTrackPoint(boat.getCurrentPosition());
}
//Set finish time if boat finished.
if (boatStatusEnum == BoatStatusEnum.FINISHED || legNumber == this.legs.size()) {
boat.setTimeFinished(boatLocation.getTime());
boat.setStatus(BoatStatusEnum.FINISHED);
}
}
}
/**
* Updates all of the marker boats based on messages received.
* @param boatMarkers The list of marker boats.
* @param boatLocationMap A map between boat sourceIDs and BoatLocation messages.
* @param boatStatusMap A map between boat sourceIDs and BoatStatus messages.
*/
private void updateMarkers(ObservableList<Mark> boatMarkers, Map<Integer, BoatLocation> boatLocationMap, Map<Integer, BoatStatus> boatStatusMap) {
for (Mark mark : boatMarkers) {
BoatLocation boatLocation = boatLocationMap.get(mark.getSourceID());
updateMark(mark, boatLocation);
}
}
/**
* Updates an individual marker boat based on messages received.
* @param mark The marker boat to be updated.
* @param boatLocation The message describing the boat's new location.
*/
private void updateMark(Mark mark, BoatLocation boatLocation) {
if (boatLocation != null) {
//We only update the boat's position.
double latitude = boatLocation.getLatitudeDouble();
double longitude = boatLocation.getLongitudeDouble();
GPSCoordinate gpsCoordinate = new GPSCoordinate(latitude, longitude);
mark.setPosition(gpsCoordinate);
}
}
/**
* Updates the race status (RaceStatusEnum, wind bearing, wind speed) based on received messages.
* @param raceStatus The RaceStatus message received.
*/
private void updateRaceStatus(RaceStatus raceStatus) {
//Race status enum.
this.raceStatusEnum = RaceStatusEnum.fromByte(raceStatus.getRaceStatus());
//Wind bearing.
this.windDirection.setDegrees(raceStatus.getScaledWindDirection());
//Wind speed.
this.windSpeed = raceStatus.getWindSpeedKnots();
}
public void setController(RaceController controller) {
this.controller = controller;
}
/**
* Runnable for the thread.
*/
public void run() {
setControllerListeners();
Platform.runLater(() -> controller.createSparkLine(boats));
initialiseBoats();
startRaceStream();
}
/**
* Update the calculated fps to the fps label
*
* @param fps The new calculated fps value
*/
private void updateFPS(int fps) {
Platform.runLater(() -> controller.setFrames("FPS: " + fps));
}
/**
* Starts the Race Simulation, playing the race start to finish with the timescale.
* This prints the boats participating, the order that the events occur in time order, and the respective information of the events.
*/
private void startRaceStream() {
System.setProperty("javafx.animation.fullspeed", "true");
new AnimationTimer() {
final long timeRaceStarted = System.currentTimeMillis(); //start time of loop
int fps = 0; //init fps value
long timeCurrent = System.currentTimeMillis(); //current time
@Override
public void handle(long arg0) {
totalTimeElapsed = System.currentTimeMillis() - timeRaceStarted;
//Update racing boats.
updateBoats(boats, visualiserInput.getBoatLocationMap(), visualiserInput.getBoatStatusMap());
//And their positions (e.g., 5th).
updateBoatPositions(boats);
//Update marker boats.
updateMarkers(boatMarkers, visualiserInput.getBoatLocationMap(), visualiserInput.getBoatStatusMap());
//Update race status.
updateRaceStatus(visualiserInput.getRaceStatus());
//TODO tidy this circular dependency up
if (getRaceStatusEnum() == RaceStatusEnum.FINISHED) {
controller.finishRace(boats);
stop();
}
controller.updateMap(boats, boatMarkers);
fps++;
if ((System.currentTimeMillis() - timeCurrent) > 1000) {
updateFPS(fps);
lastFPS = fps;
fps = 0;
timeCurrent = System.currentTimeMillis();
}
}
}.start();
}
/**
* Update position of boats in race (e.g, 5th), no position if on starting leg or DNF.
*/
private void updateBoatPositions(ObservableList<VisualiserBoat> boats) {
sortBoatsByPosition(boats);
for (VisualiserBoat boat: boats) {
boat.setPosition(Integer.toString(boats.indexOf(boat) + 1));
if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0))
boat.setPosition("-");
}
}
/**
* Sorts the list of boats by their position within the race.
* @param boats The list of boats in the race.
*/
private void sortBoatsByPosition(ObservableList<VisualiserBoat> boats) {
FXCollections.sort(boats, (a, b) -> {
//Get the difference in leg numbers.
int legNumberDelta = b.getCurrentLeg().getLegNumber() - a.getCurrentLeg().getLegNumber();
//If they're on the same leg, we need to compare time to finish leg.
if (legNumberDelta == 0) {
return (int) (b.getEstimatedTime() - a.getEstimatedTime());
} else {
return legNumberDelta;
}
});
}
/**
* Update call for the controller.
*/
private void setControllerListeners() {
if (controller != null) controller.setInfoTable(this);
}
/**
* Returns the boats participating in the race.
* @return ObservableList of boats participating in the race.
*/
public ObservableList<VisualiserBoat> getBoats() {
return boats;
}
/**
* Takes an estimated time an event will occur, and converts it to the
* number of seconds before the event will occur.
*
* @param estTimeMillis estimated time in milliseconds
* @return int difference between time the race started and the estimated time
*/
private int convertEstTime(long estTimeMillis, long currentTime) {
long estElapsedMillis = estTimeMillis - currentTime;
int estElapsedSecs = Math.round(estElapsedMillis/1000);
return estElapsedSecs;
}
}