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.
main
fjc40 9 years ago
parent 7f027c8cc5
commit 8d36d89570

@ -3,6 +3,7 @@ package mock.app;
import mock.model.Polars;
import network.Messages.Enums.MessageType;
import org.xml.sax.SAXException;
import shared.model.Constants;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
@ -66,15 +67,24 @@ public class Event {
}
/**
* Sets the xml description of the race to show the race was created now, and starts in 3 minutes
* Sets the xml description of the race to show the race was created now, and starts in 4 minutes
* @param raceXML
* @return String containing edited xml
*/
private String getRaceXMLAtCurrentTime(String raceXML) {
//The start time is current time + 4 minutes. prestart is 3 minutes, and we add another minute.
long millisecondsToAdd = Constants.RacePreStartTime + (1 * 60 * 1000);
long secondsToAdd = millisecondsToAdd / 1000;
DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
ZonedDateTime creationTime = ZonedDateTime.now();
return raceXML.replace("CREATION_TIME", dateFormat.format(creationTime))
.replace("START_TIME", dateFormat.format(creationTime.plusMinutes(3)));
raceXML.replace("CREATION_TIME", dateFormat.format(creationTime));
raceXML.replace("START_TIME", dateFormat.format(creationTime.plusSeconds(secondsToAdd)));
return raceXML;
}
}

@ -1,289 +0,0 @@
package mock.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.Exceptions.StreamedCourseXMLException;
import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.text.ParseException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* XML Reader that reads in the race data required for this race
*/
public class RaceXMLReader extends XMLReader implements RaceDataSource {
private static final double COORDINATEPADDING = 0.000;
private GPSCoordinate mapTopLeft, mapBottomRight;
private final List<GPSCoordinate> boundary = new ArrayList<>();
private final Map<Integer,Element> compoundMarkMap = new HashMap<>();
private final Map<Integer, Boat> participants = new HashMap<>();
private final List<Leg> legs = new ArrayList<>();
private final List<CompoundMark> compoundMarks = new ArrayList<>();
private ZonedDateTime creationTimeDate;
private ZonedDateTime raceStartTime;
private int raceID;
private String raceType;
private boolean postpone;
private Map<Integer, Boat> boats;
private Map<Integer, Mark> marks;
/**
* Constructor for Streamed Race XML
* @param filePath path of the file
* @param boatData data for boats in race
* @throws IOException error
* @throws SAXException error
* @throws ParserConfigurationException error
* @throws ParseException error
* @throws StreamedCourseXMLException error
*/
public RaceXMLReader(String filePath, BoatDataSource boatData) throws IOException, SAXException, ParserConfigurationException, ParseException, StreamedCourseXMLException {
this(filePath, boatData, true);
}
/**
* Constructor for Streamed Race XML
* @param filePath file path to read
* @param boatData data of the boats in race
* @param read whether or not to read and store the files straight away.
* @throws IOException error
* @throws SAXException error
* @throws ParserConfigurationException error
* @throws ParseException error
* @throws StreamedCourseXMLException error
*/
public RaceXMLReader(String filePath, BoatDataSource boatData, boolean read) throws IOException, SAXException, ParserConfigurationException, ParseException, StreamedCourseXMLException {
super(filePath);
this.boats = boatData.getBoats();
this.marks = boatData.getMarkerBoats();
if (read) {
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 in the participants for htis 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, boats.get(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.
* @see CompoundMark
*/
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);
compoundMarkMap.put(compoundMarkID, (Element)compoundMark);
compoundMarks.add(getCompoundMark(compoundMarkID));
}
}
}
/**
* Generates a CompoundMark from the CompoundMark element with given ID.
* @param compoundMarkID index of required CompoundMark element
* @return generated CompoundMark
* @throws StreamedCourseXMLException if CompoundMark element contains unhandled number of compoundMarks
* @see CompoundMark
*/
private CompoundMark getCompoundMark(int compoundMarkID) throws StreamedCourseXMLException {
Element compoundMark = compoundMarkMap.get(compoundMarkID);
NodeList nMarks = compoundMark.getElementsByTagName("Mark");
CompoundMark marker;
switch(nMarks.getLength()) {
case 1: marker = new CompoundMark(getMark((Element)nMarks.item(0)));
break;
case 2: marker = new CompoundMark(getMark((Element)nMarks.item(0)), getMark((Element)nMarks.item(1))); break;
default: throw new StreamedCourseXMLException();
}
return marker;
}
/**
* Gets a mark from an Element
* @param mark Element the mark is suppose to be part of
* @return a Mark that existed in the element
*/
private Mark getMark(Element mark) {
int sourceID = Integer.parseInt(mark.getAttribute("SourceID"));
return marks.get(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 compoundMarkMap.get(compoundMarkID).getAttribute("Name");
}
/**
* Populates list of legs given CompoundMarkSequence element and referenced CompoundMark elements.
* @throws StreamedCourseXMLException if compoundMarks 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);
CompoundMark lastCompoundMark = getCompoundMark(getCompoundMarkID(markXML));
String legName = getCompoundMarkName(getCompoundMarkID(markXML));
for(int i = 1; i < nCorners.getLength(); i++) {
markXML = (Element)nCorners.item(i);
CompoundMark currentCompoundMark = getCompoundMark(getCompoundMarkID(markXML));
legs.add(new Leg(legName, lastCompoundMark, currentCompoundMark, i-1));
lastCompoundMark = currentCompoundMark;
legName = getCompoundMarkName(getCompoundMarkID(markXML));
}
}
/**
* Reads the boundary limits of the course
*/
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<CompoundMark> getCompoundMarks() { return compoundMarks; }
public Double getPadding() {
return COORDINATEPADDING;
}
public ZonedDateTime getCreationTimeDate() {
return creationTimeDate;
}
public ZonedDateTime getZonedDateTime() {
return raceStartTime;
}
public int getRaceId() {
return raceID;
}
public String getRaceType() {
return raceType;
}
public boolean isPostpone() {
return postpone;
}
public List<Boat> getBoats() {
return new ArrayList<>(participants.values());
}
}

@ -1,7 +0,0 @@
package mock.exceptions;
/**
* Created by cbt24 on 25/04/17.
*/
public class StreamedCourseXMLException extends Throwable {
}

@ -19,7 +19,6 @@ public class MockBoat extends Boat {
/**
* This stores the milliseconds since the boat has changed its tack, to allow for only updating the tack every X milliseconds.
* TODO milliseconds
*/
private long timeSinceTackChange = 0;
@ -40,6 +39,19 @@ public class MockBoat extends Boat {
}
/**
* Constructs a mock boat object from a given boat and polars table.
*
* @param boat The boat to convert into a MockBoat.
* @param polars The polars table to use for this boat.
*/
public MockBoat(Boat boat, Polars polars) {
super(boat.getSourceID(), boat.getName(), boat.getCountry());
this.polars = polars;
}
/**

@ -1,20 +1,18 @@
package mock.model;
import javafx.animation.AnimationTimer;
import javafx.collections.FXCollections;
import mock.app.MockOutput;
import network.Messages.BoatStatus;
import network.Messages.Enums.BoatStatusEnum;
import network.Messages.RaceStatus;
import network.Utils.AC35UnitConverter;
import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.Enums.RaceTypeEnum;
import shared.model.Bearing;
import shared.model.Constants;
import shared.model.GPSCoordinate;
import shared.model.Leg;
import shared.dataInput.RegattaDataSource;
import shared.model.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.*;
import static java.lang.Math.cos;
@ -24,42 +22,20 @@ import static java.lang.Math.cos;
* Has a course, boats, boundaries, etc...
* Is responsible for simulating the race, and sending messages to a MockOutput instance.
*/
public class Race implements Runnable {
public class MockRace extends Race {
/**
* An observable list of boats in the race.
*/
private ObservableList<Boat> boats;
private List<MockBoat> boats;
/**
* An observable list of compound marks in the race.
*/
private ObservableList<CompoundMark> compoundMarks;
/**
* A list of legs in the race.
*/
private List<Leg> legs;
/**
* A list of coordinates describing the boundary of the course.
*/
private List<GPSCoordinate> boundary;
/**
* A copy of the boundary list, except "shrunk" inwards by 50m.
*/
private List<GPSCoordinate> shrinkBoundary;
/**
* The elapsed time, in milliseconds, of the race.
*/
private long totalTimeElapsed;
/**
* The starting timestamp, in milliseconds, of the race.
*/
private long startTime;
/**
* The scale factor of the race.
@ -68,20 +44,6 @@ public class Race implements Runnable {
*/
private int scaleFactor = 5;
/**
* The race ID of the course.
*/
private int raceId;
/**
* The current status of the race.
*/
private RaceStatusEnum raceStatusEnum;
/**
* The type of race this is.
*/
private RaceTypeEnum raceType;
/**
* The percent chance that a boat fails the race, and enters a DNF state, at each checkpoint.
@ -96,66 +58,95 @@ public class Race implements Runnable {
private MockOutput mockOutput;
/**
* Wind direction bearing.
* Used to generate random numbers when changing the wind direction.
*/
private Bearing windDirection;
private int changeWind = 4;
/**
* Wind speed (knots).
* Convert this to millimeters per second before passing to RaceStatus.
* The bearing the wind direction starts at.
*/
private double windSpeed;
private static final Bearing windBaselineBearing = Bearing.fromDegrees(225);
/**
* The lower bearing angle that the wind may have.
*/
private static final Bearing windLowerBound = Bearing.fromDegrees(215);
/**
* The upper bearing angle that the wind may have.
*/
private static final Bearing windUpperBound = Bearing.fromDegrees(235);
private double windDirDegrees;
private double windDir;
private int changeWind = 4;
private static final int windUpperBound = 235;
private static final int windLowerBound = 215;
/**
* Constructs a race object with a given RaceDataSource and sends events to the given mockOutput.
* @param raceData Data source for race related data (boats, legs, etc...).
* Constructs a race object with a given RaceDataSource, BoatDataSource, and RegattaDataSource and sends events to the given mockOutput.
* @param boatDataSource Data source for boat related data (yachts and marker boats).
* @param raceDataSource Data source for race related data (participating boats, legs, etc...).
* @param regattaDataSource Data source for race related data (course name, location, timezone, etc...).
* @param polars The polars table to be used for boat simulation.
* @param mockOutput The mockOutput to send events to.
*/
public Race(RaceDataSource raceData, MockOutput mockOutput) {
public MockRace(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource, Polars polars, MockOutput mockOutput) {
super(boatDataSource, raceDataSource, regattaDataSource);
this.mockOutput = mockOutput;
this.boats = FXCollections.observableArrayList(raceData.getBoats());
this.compoundMarks = FXCollections.observableArrayList(raceData.getCompoundMarks());
this.boundary = raceData.getBoundary();
this.boats = this.generateMockBoats(boatDataSource.getBoats(), raceDataSource.getParticipants(), polars);
this.shrinkBoundary = GPSCoordinate.getShrinkBoundary(this.boundary);
this.legs = raceData.getLegs();
this.legs.add(new Leg("Finish", this.legs.size()));
this.raceId = raceData.getRaceId();
this.windSpeed = 12;
this.windDirection = Bearing.fromDegrees(180);
//The start time is current time + 4 minutes, scaled. prestart is 3 minutes, and we add another.
this.startTime = System.currentTimeMillis() + ((Constants.RacePreStartTime + (1 * 60 * 1000)) / this.scaleFactor);
this.setRaceStatusEnum(RaceStatusEnum.NOT_ACTIVE);
this.raceType = raceData.getRaceType();
}
this.windSpeed = 12;
this.windDirection = Bearing.fromDegrees(180);
/**
* Generates a list of MockBoats 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 polars The polars table to be used for boat simulation.
* @return A list of MockBoats that are participating in the race.
*/
private List<MockBoat> generateMockBoats(Map<Integer, Boat> boats, List<Integer> sourceIDs, Polars polars) {
List<MockBoat> mockBoats = new ArrayList<>(sourceIDs.size());
//For each sourceID participating...
for (int sourceID : sourceIDs) {
//Get the boat associated with the sourceID.
Boat boat = boats.get(sourceID);
//Construct a MockBoat using the Boat and Polars.
MockBoat mockBoat = new MockBoat(boat, polars);
mockBoats.add(mockBoat);
}
return mockBoats;
}
/**
* Runnable for the thread.
*/
public void run() {
initialiseBoats();
initialiseWindDir();
initialiseWindDirection();
countdownTimer.start();
}
/**
* Parse the compound marker boats through mock output.
*/
@ -184,7 +175,14 @@ public class Race implements Runnable {
*/
private void parseIndividualMark(Mark mark) {
this.mockOutput.parseBoatLocation(mark.getSourceID(), mark.getPosition().getLatitude(), mark.getPosition().getLongitude(),0,0, totalTimeElapsed+startTime);
this.mockOutput.parseBoatLocation(
mark.getSourceID(),
mark.getPosition().getLatitude(),
mark.getPosition().getLongitude(),
0,
0,
this.totalTimeElapsed + this.startTime
);
}
@ -194,7 +192,7 @@ public class Race implements Runnable {
private void parseBoatLocations() {
//Parse each boat.
for (Boat boat : this.boats) {
for (MockBoat boat : this.boats) {
this.parseIndividualBoatLocation(boat);
@ -206,7 +204,7 @@ public class Race implements Runnable {
* Parses an individual boat, and sends it to mockOutput.
* @param boat The boat to parse.
*/
private void parseIndividualBoatLocation(Boat boat) {
private void parseIndividualBoatLocation(MockBoat boat) {
this.mockOutput.parseBoatLocation(
boat.getSourceID(),
@ -263,9 +261,13 @@ public class Race implements Runnable {
List<BoatStatus> boatStatuses = new ArrayList<>();
//Add each boat status to the status list.
for (Boat boat : boats) {
for (MockBoat boat : boats) {
BoatStatus boatStatus = new BoatStatus(boat.getSourceID(), boat.getStatus(), boat.getCurrentLeg().getLegNumber(), boat.getEstimatedTime());
BoatStatus boatStatus = new BoatStatus(
boat.getSourceID(),
boat.getStatus(),
boat.getCurrentLeg().getLegNumber(),
boat.getEstimatedTime() );
boatStatuses.add(boatStatus);
}
@ -277,7 +279,14 @@ public class Race implements Runnable {
int windSpeedInt = (int) (windSpeed * Constants.KnotsToMMPerSecond);
//Create race status object, and send it.
RaceStatus raceStatus = new RaceStatus(System.currentTimeMillis(), this.raceId, this.getRaceStatusEnum().getValue(), this.startTime, windDirectionInt, windSpeedInt, this.getRaceType().getValue(), boatStatuses);
RaceStatus raceStatus = new RaceStatus(
System.currentTimeMillis(),
this.raceId,
this.getRaceStatusEnum().getValue(),
this.startTime, windDirectionInt,
windSpeedInt,
this.getRaceType().getValue(),
boatStatuses );
mockOutput.parseRaceStatus(raceStatus);
@ -290,7 +299,7 @@ public class Race implements Runnable {
*/
private void setBoatsStatusToRacing() {
for (Boat boat : this.boats) {
for (MockBoat boat : this.boats) {
boat.setStatus(BoatStatusEnum.RACING);
}
}
@ -317,14 +326,13 @@ public class Race implements Runnable {
parseMarks();
// Change wind direction
changeWindDir();
changeWindDirection();
//Parse the race status.
parseRaceStatus();
if (getRaceStatusEnum() == RaceStatusEnum.STARTED) {
System.setProperty("javafx.animation.fullspeed", "true");
setBoatsStatusToRacing();
raceTimer.start();
this.stop();
@ -365,12 +373,12 @@ public class Race implements Runnable {
//Get the time period of this frame.
long framePeriod = currentTime - lastFrameTime;
//We actually simulate 20ms istead of the amount of time that has occurred, as that ensure that we don't end up with large frame periods on slow computers, causing position issues.
//We actually simulate 20ms instead of the amount of time that has occurred, as that ensure that we don't end up with large frame periods on slow computers, causing position issues.
framePeriod = 20;
//For each boat, we update its position, and generate a BoatLocationMessage.
for (Boat boat : boats) {
for (MockBoat boat : boats) {
//If it is still racing, update its position.
if (boat.getStatus() == BoatStatusEnum.RACING) {
@ -390,7 +398,7 @@ public class Race implements Runnable {
if (getNumberOfActiveBoats() != 0) {
// Change wind direction
changeWindDir();
changeWindDirection();
//Parse the boat locations.
parseBoatLocations();
@ -415,8 +423,9 @@ public class Race implements Runnable {
int iters = 0;
@Override
public void handle(long now) {
RaceStatus raceStatus = new RaceStatus(System.currentTimeMillis(), raceId, 4, startTime, 0, 2300, 2, new ArrayList<>());
mockOutput.parseRaceStatus(raceStatus);
parseRaceStatus();
if (iters > 500) {
mockOutput.stop();
stop();
@ -425,24 +434,26 @@ public class Race implements Runnable {
}
};
/**
* Initialise the boats in the race.
* This sets their starting positions and current legs.
*/
public void initialiseBoats() {
@Override
protected void initialiseBoats() {
//Gets the starting positions of the boats.
List<GPSCoordinate> startingPositions = getSpreadStartingPositions();
//Get iterators for our boat and position lists.
Iterator<Boat> boatIt = this.boats.iterator();
Iterator<MockBoat> boatIt = this.boats.iterator();
Iterator<GPSCoordinate> startPositionIt = startingPositions.iterator();
//Iterate over the pair of lists.
while (boatIt.hasNext() && startPositionIt.hasNext()) {
//Get the next boat and position.
Boat boat = boatIt.next();
MockBoat boat = boatIt.next();
GPSCoordinate startPosition = startPositionIt.next();
@ -522,7 +533,7 @@ public class Race implements Runnable {
* @param bearingBounds An array containing the lower and upper acceptable bearing bounds to keep the boat in the course.
* @return VMG for the specified boat.
*/
private VMG calculateVMG(Boat boat, Bearing[] bearingBounds) {
private VMG calculateVMG(MockBoat boat, Bearing[] bearingBounds) {
//Get the lower and upper acceptable bounds.
Bearing lowerAcceptableBound = bearingBounds[0];
@ -533,7 +544,6 @@ public class Race implements Runnable {
VMG bestVMG = boat.getPolars().calculateVMG(this.windDirection, this.windSpeed, boat.calculateBearingToNextMarker(), lowerAcceptableBound, upperAcceptableBound);
return bestVMG;
}
@ -572,7 +582,7 @@ public class Race implements Runnable {
* @param vmg The new VMG to test.
* @return True if the new VMG is improves velocity, false otherwise.
*/
private boolean improvesVelocity(Boat boat, VMG vmg) {
private boolean improvesVelocity(MockBoat boat, VMG vmg) {
//Get the boats "current" VMG.
VMG boatVMG = new VMG(boat.getCurrentSpeed(), boat.getBearing());
@ -590,7 +600,7 @@ public class Race implements Runnable {
* @param updatePeriodMilliseconds The time, in milliseconds, since the last update.
* @param totalElapsedMilliseconds The total number of milliseconds that have elapsed since the start of the race.
*/
protected void updatePosition(Boat boat, long updatePeriodMilliseconds, long totalElapsedMilliseconds) {
protected void updatePosition(MockBoat boat, long updatePeriodMilliseconds, long totalElapsedMilliseconds) {
//Checks if the current boat has finished the race or not.
boolean finish = this.isLastLeg(boat.getCurrentLeg());
@ -649,7 +659,7 @@ public class Race implements Runnable {
* @param boat The boat to check.
* @return An array of bearings. The first is the lower bound, the second is the upper bound.
*/
private Bearing[] calculateBearingBounds(Boat boat) {
private Bearing[] calculateBearingBounds(MockBoat boat) {
Bearing[] bearings = new Bearing[2];
@ -732,7 +742,7 @@ public class Race implements Runnable {
* @param boat The boat to check.
* @param timeElapsed The total time, in milliseconds, that has elapsed since the race started.
*/
protected void checkPosition(Boat boat, long timeElapsed) {
protected void checkPosition(MockBoat boat, long timeElapsed) {
//The distance, in nautical miles, within which the boat needs to get in order to consider that it has reached the marker.
double epsilonNauticalMiles = 100.0 / Constants.NMToMetersConversion; //100 meters. TODO should be more like 5-10.
@ -764,6 +774,7 @@ public class Race implements Runnable {
boat.setTimeFinished(timeElapsed);
boat.setCurrentSpeed(0);
boat.setStatus(BoatStatusEnum.FINISHED);
} else if (doNotFinish()) {
//Boat has pulled out of race.
boat.setTimeFinished(timeElapsed);
@ -778,26 +789,6 @@ public class Race implements Runnable {
}
/**
* Determines whether or not a specific leg is the last leg in the race.
* @param leg The leg to check.
* @return Returns true if it is the last, false otherwse.
*/
private boolean isLastLeg(Leg leg) {
//Get the last leg.
Leg lastLeg = this.legs.get(this.legs.size() - 1);
//Check its ID.
int lastLegID = lastLeg.getLegNumber();
//Get the specified leg's ID.
int legID = leg.getLegNumber();
//Check if they are the same.
return legID == lastLegID;
}
/**
@ -821,31 +812,6 @@ public class Race implements Runnable {
}
/**
* Returns the current race status.
* @return The current race status.
*/
public RaceStatusEnum getRaceStatusEnum() {
return raceStatusEnum;
}
/**
* Sets the current race status.
* @param raceStatusEnum The new status of the race.
*/
private void setRaceStatusEnum(RaceStatusEnum raceStatusEnum) {
this.raceStatusEnum = raceStatusEnum;
}
/**
* Returns the type of race this is.
* @return The type of race this is.
*/
public RaceTypeEnum getRaceType() {
return raceType;
}
/**
* Returns the number of boats that are still active in the race.
@ -854,74 +820,81 @@ public class Race implements Runnable {
*/
protected int getNumberOfActiveBoats() {
int numberofActiveBoats = 0;
int numberOfActiveBoats = 0;
for (Boat boat : this.boats) {
for (MockBoat boat : this.boats) {
//If the boat is currently racing, count it.
if (boat.getStatus() == BoatStatusEnum.RACING) {
numberofActiveBoats++;
numberOfActiveBoats++;
}
}
return numberofActiveBoats;
return numberOfActiveBoats;
}
/**
* Returns an observable list of boats in the race.
* Returns a list of boats in the race.
* @return List of boats in the race.
*/
public ObservableList<Boat> getBoats() {
public List<MockBoat> getBoats() {
return boats;
}
protected void initialiseWindDir(){
windDirDegrees = 225;
windDir = AC35UnitConverter.convertHeading(windDirDegrees);
/*windDir = new Random().nextInt(65535+1);
windDir = BoatLocation.convertHeadingIntToDouble(255);*/
this.windDirection = new Bearing((int)windDir);
/**
* Initialises the wind bearing with the value of the windBaselineBearing.
*/
protected void initialiseWindDirection() {
//Set the starting bearing.
this.windDirection = Bearing.fromDegrees(MockRace.windBaselineBearing.degrees());
}
protected void changeWindDir(){
/**
* Changes the wind direction randomly, while keeping it within [windLowerBound, windUpperBound].
*/
protected void changeWindDirection() {
//Randomly add or remove 0.5 degrees.
int r = new Random().nextInt(changeWind) + 1;
if (r == 1) {
windDirDegrees = (0.5 + windDirDegrees) % 360;
//Add 0.5 degrees to the wind bearing.
this.windDirection.setDegrees(this.windDirection.degrees() + 0.5);
} else if (r == 2) {
windDirDegrees = ((windDirDegrees - 0.5) + 360) % 360;///keep the degrees positive when below 0
}
if (windDirDegrees > windUpperBound){
windDirDegrees = windUpperBound;
}
if (windDirDegrees < windLowerBound){
windDirDegrees = windLowerBound;
}
//Minus 0.5 degrees from the wind bearing.
this.windDirection.setDegrees(this.windDirection.degrees() - 0.5);
windDir = AC35UnitConverter.convertHeading(windDirDegrees);
this.windDirection = new Bearing(windDirDegrees);
}
protected void setChangeWind(int changeVal){
if (changeVal>=0){
changeWind = changeVal;
//Ensure that the wind is in the correct bounds.
if (this.windDirection.degrees() > MockRace.windUpperBound.degrees()) {
this.windDirection.setBearing(MockRace.windUpperBound);
} else if (this.windDirection.degrees() < MockRace.windLowerBound.degrees()) {
this.windDirection.setBearing(MockRace.windLowerBound);
}
}
protected int getWind(){
return (int)windDir;
}
/**
* Updates the boat's estimated time to next mark if positive
* @param boat to estimate time given its velocity
*/
private void updateEstimatedTime(Boat boat) {
private void updateEstimatedTime(MockBoat boat) {
double velocityToMark = boat.getCurrentSpeed() * cos(boat.getBearing().radians() - boat.calculateBearingToNextMarker().radians()) / Constants.KnotsToMMPerSecond;
if (velocityToMark > 0) {
long timeFromNow = (long) (1000 * boat.calculateDistanceToNextMarker() / velocityToMark);
boat.setEstimatedTime(startTime + totalTimeElapsed + timeFromNow);
}
}
}

@ -1,7 +1,8 @@
package network.Messages;
import seng302.Networking.Messages.Enums.BoatStatusEnum;
import seng302.Networking.Utils.ByteConverter;
import network.Messages.Enums.BoatStatusEnum;
import network.Utils.ByteConverter;
/**
* Created by hba56 on 23/04/17.

@ -22,7 +22,7 @@ public enum RaceTypeEnum {
/**
* Used to indicate that a given byte value is invalid.
*/
NOT_A_STATUS(-1);
NOT_A_RACE_TYPE(-1);
/**
@ -48,6 +48,29 @@ public enum RaceTypeEnum {
}
/**
* Attempts to convert a string into a RaceTypeEnum.
* Ignores case.
* Treats anything starting with "fleet" as {@link #FLEET_RACE}, and anything starting with "match" as {@link #MATCH_RACE}.
* @param value The string to convert.
* @return The RaceTypeEnum.
*/
public static RaceTypeEnum fromString(String value) {
//Convert to lower case.
value = value.toLowerCase();
if (value.startsWith("fleet")) {
return FLEET_RACE;
} else if (value.startsWith("match")) {
return MATCH_RACE;
} else {
return NOT_A_RACE_TYPE;
}
}
/**
* Stores a mapping between Byte values and RaceStatusEnum values.
*/
@ -74,8 +97,8 @@ public enum RaceTypeEnum {
RaceTypeEnum type = RaceTypeEnum.byteToStatusMap.get(raceTypeEnum);
if (type == null) {
//If the byte value wasn't found, return the NOT_A_STATUS RaceTypeEnum.
return RaceTypeEnum.NOT_A_STATUS;
//If the byte value wasn't found, return the NOT_A_RACE_TYPE RaceTypeEnum.
return RaceTypeEnum.NOT_A_RACE_TYPE;
} else {
//Otherwise, return the RaceTypeEnum.
return type;

@ -1,7 +1,9 @@
package network.Messages;
import seng302.Networking.Messages.Enums.MessageType;
import seng302.Networking.Utils.AC35UnitConverter;
import network.Messages.Enums.MessageType;
import network.Utils.AC35UnitConverter;
import shared.model.Constants;
import java.util.List;
@ -12,7 +14,7 @@ public class RaceStatus extends AC35Data {
private long currentTime;
private int raceID;
private int raceStatus;
private byte raceStatus;
private long expectedStartTime;
private int windDirection;
private int windSpeed;
@ -49,7 +51,7 @@ public class RaceStatus extends AC35Data {
*
* @return race status number
*/
public int getRaceStatus()
public byte getRaceStatus()
{
return raceStatus;
}
@ -64,6 +66,10 @@ public class RaceStatus extends AC35Data {
return windDirection;
}
/**
* Returns the wind speed for this race status, in millimeters per second.
* @return Wind speed in millimeters per second.
*/
public int getWindSpeed()
{
return windSpeed;
@ -124,6 +130,14 @@ public class RaceStatus extends AC35Data {
}
public double getScaledWindDirection() {
return (double) AC35UnitConverter.convertHeading(windDirection);
return AC35UnitConverter.convertHeading(windDirection);
}
/**
* Returns the wind speed for this race status, in knots.
* @return Wind speed in knots.
*/
public double getWindSpeedKnots() {
return (windSpeed / Constants.KnotsToMMPerSecond);
}
}

@ -2,11 +2,13 @@ package shared.dataInput;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import shared.exceptions.InvalidBoatDataException;
import shared.exceptions.XMLReaderException;
import shared.model.Boat;
import shared.model.GPSCoordinate;
import shared.model.Mark;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
@ -27,15 +29,43 @@ public class BoatXMLReader extends XMLReader implements BoatDataSource {
/**
* Constructor for Boat XML
* Constructor for Boat XML using a file read as a resource.
*
* @param filePath Name/path of file to read. Read as a resource.
* @throws XMLReaderException Thrown if the file cannot be parsed.
* @throws InvalidBoatDataException Thrown if the file cannot be parsed correctly.
*/
public BoatXMLReader(String filePath) throws XMLReaderException {
public BoatXMLReader(String filePath) throws XMLReaderException, InvalidBoatDataException {
super(filePath);
//Attempt to read boat xml file.
try {
read();
} catch (Exception e) {
throw new InvalidBoatDataException("An error occurred while reading the boat xml file", e);
}
}
/**
* Constructor for Boat XML, using an InputStream.
*
* @param fileStream Stream to read boat data from.
* @throws XMLReaderException Thrown if the file cannot be parsed.
* @throws InvalidBoatDataException Thrown if the stream cannot be parsed correctly.
*/
public BoatXMLReader(InputStream fileStream) throws XMLReaderException, InvalidBoatDataException {
super(fileStream);
//Attempt to read boat xml stream.
try {
read();
} catch (Exception e) {
throw new InvalidBoatDataException("An error occurred while reading the boat xml stream", e);
}
}
/**
* Read the XML

@ -17,10 +17,10 @@ import java.util.List;
*/
public interface RaceDataSource {
/**
* Returns the list of boats competing in the race.
* @return Boats competing in the race.
* Returns the list of sourceIDs for boats competing in the race.
* @return SourceIDs for boats competing in the race.
*/
List<Boat> getBoats();
List<Integer> getParticipants();
/**
* Returns the list of legs in the race.
@ -36,10 +36,11 @@ public interface RaceDataSource {
/**
* Returns a list of CompoundMarks in the race.
* @return
* @return The sequence of compounds marks in the race.
*/
List<CompoundMark> getCompoundMarks();
/**
* Returns the ID of the race.
* @return The ID of the race.
@ -52,11 +53,25 @@ public interface RaceDataSource {
*/
RaceTypeEnum getRaceType();
/**
* Returns the start time/date of the race.
* @return The race's start time.
*/
ZonedDateTime getZonedDateTime();
ZonedDateTime getStartDateTime();
/**
* Returns the creation time/date of the race xml file.
* @return The race xml file's creation time.
*/
ZonedDateTime getCreationDateTime();
/**
* Returns whether or not the race has been postponed.
* @return True if the race has been postponed, false otherwise.
*/
boolean getPostponed();
/**
* Returns the GPS coordinate of the top left of the race map area.

@ -0,0 +1,465 @@
package shared.dataInput;
import network.Messages.Enums.RaceTypeEnum;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import shared.exceptions.InvalidRaceDataException;
import shared.exceptions.XMLReaderException;
import shared.model.*;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* Xml Reader class for Race XML used for the race.
*/
public class RaceXMLReader extends XMLReader implements RaceDataSource {
/**
* The GPS coordinate of the top left of the race boundary.
*/
private GPSCoordinate mapTopLeft;
/**
* The GPS coordinate of the bottom right of the race boundary.
*/
private GPSCoordinate mapBottomRight;
/**
* A list of GPS coordinates that make up the boundary of the race.
*/
private final List<GPSCoordinate> boundary = new ArrayList<>();
/**
* A map between compoundMarkID and a CompoundMark for all CompoundMarks in a race.
*/
private final Map<Integer, CompoundMark> compoundMarkMap = new HashMap<>();
/**
* A list of boat sourceIDs participating in the race.
*/
private final List<Integer> participants = new ArrayList<>();
/**
* A list of legs in the race.
*/
private final List<Leg> legs = new ArrayList<>();
/**
* The time that the race.xml file was created.
*/
private ZonedDateTime creationTimeDate;
/**
* The time that the race should start at, if it hasn't been postponed.
*/
private ZonedDateTime raceStartTime;
/**
* Whether or not the race has been postponed.
*/
private boolean postpone;
/**
* The ID number of the race.
*/
private int raceID;
/**
* The type of the race.
*/
private RaceTypeEnum raceType;
//TODO maybe remove these?
private Map<Integer, Boat> boats;
private Map<Integer, Mark> marks;
/**
* Constructor for Streamed Race XML
* @param filePath file path to read
* @param boatData data of the boats in race
* @throws XMLReaderException Thrown if an XML reader cannot be constructed for the given file.
* @throws InvalidRaceDataException Thrown if the XML file is invalid in some way.
*/
public RaceXMLReader(String filePath, BoatDataSource boatData) throws XMLReaderException, InvalidRaceDataException {
super(filePath);
this.boats = boatData.getBoats();
this.marks = boatData.getMarkerBoats();
//Attempt to read race xml file.
try {
read();
} catch (Exception e) {
throw new InvalidRaceDataException("An error occurred while reading the race xml file", e);
}
}
/**
* Reads the contents of the race xml file.
*/
private void read() {
readRace();
readParticipants();
readCourse();
}
/**
* Reads race related data from the race xml file.
*/
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");
}
//Race ID.
raceID = Integer.parseInt(getTextValueOfNode(settings, "RaceID"));
//Race type.
String raceTypeString = getTextValueOfNode(settings, "RaceType");
raceType = RaceTypeEnum.valueOf(raceTypeString);
//XML creation time.
creationTimeDate = ZonedDateTime.parse(getTextValueOfNode(settings, "CreationTimeDate"), dateFormat);
//Race start time.
if (raceTimeTag.getNamedItem("Time") != null) {
raceStartTime = ZonedDateTime.parse(raceTimeTag.getNamedItem("Time").getTextContent(), dateFormat);
} else {
raceStartTime = ZonedDateTime.parse(raceTimeTag.getNamedItem("Start").getTextContent(), dateFormat);
}
//Postpone status.
postpone = Boolean.parseBoolean(raceTimeTag.getNamedItem("Postpone").getTextContent());
}
/**
* Reads in the participants for this race.
*/
private void readParticipants() {
//Gets the "<Participants>..</Participants>" element.
Element participants = (Element) doc.getElementsByTagName("Participants").item(0);
//Gets the number of participants.
int numberOfParticipants = participants.getChildNodes().getLength();
//For each participant, read its sourceID.
for (int i = 0; i < numberOfParticipants; i++) {
//Get the participating yacht.
Node yacht = participants.getChildNodes().item(i);
if (yacht.getNodeName().equals("Yacht")) {
if (exists(yacht, "SourceID")) {
//If the node is a valid yacht with a sourceID, add it to participant list.
int sourceID = Integer.parseInt(yacht.getAttributes().getNamedItem("SourceID").getTextContent());
this.participants.add(sourceID);
}
}
}
}
/**
* Reads course data from the xml file.
*/
private void readCourse() {
readCompoundMarks();
readCompoundMarkSequence();
readCourseLimits();
readMapTopLeft();
readMapBottomRight();
}
/**
* Indexes CompoundMark elements by their ID for use in generating the course, and populates list of Markers.
* @see CompoundMark
*/
private void readCompoundMarks() {
//Gets the "<Course>...</..>" element.
Element course = (Element) doc.getElementsByTagName("Course").item(0);
//Number of compound marks in the course.
int numberOfCompoundMarks = course.getChildNodes().getLength();
//For each CompoundMark element, create a CompoundMark object.
for(int i = 0; i < numberOfCompoundMarks; i++) {
//Get the CompoundMark element.
Element compoundMarkElement = (Element) course.getChildNodes().item(i);
//If it is actually a CompoundMark element, turn it into a CompoundMark object.
if(compoundMarkElement.getNodeName().equals("CompoundMark")) {
CompoundMark compoundMark = createCompoundMark(compoundMarkElement);
compoundMarkMap.put(compoundMark.getId(), compoundMark);
}
}
}
/**
* Generates a CompoundMark from a given CompondMark element.
* @param compoundMarkElement The CompoundMark element to turn into a CompoundMark object.
* @return The corresponding CompoundMark object.
* @throws InvalidRaceDataException If the element cannot be converted into a CompoundMark.
*/
private CompoundMark createCompoundMark(Element compoundMarkElement) throws InvalidRaceDataException {
//CompoundMark ID.
int compoundMarkID = getCompoundMarkID(compoundMarkElement);
//CompoundMark name.
String compoundMarkName = getCompoundMarkName(compoundMarkElement);
//Get the list of marks within the compound mark.
NodeList marks = compoundMarkElement.getElementsByTagName("Mark");
CompoundMark compoundMark;
switch(marks.getLength()) {
case 1: {
//Create the Mark sub-object.
Mark mark1 = createMark((Element) marks.item(0));
//Create compound mark.
compoundMark = new CompoundMark(compoundMarkID, compoundMarkName, mark1);
break;
} case 2: {
//Create the Mark sub-objects.
Mark mark1 = createMark((Element) marks.item(0));
Mark mark2 = createMark((Element) marks.item(1));
//Create compound mark.
compoundMark = new CompoundMark(compoundMarkID, compoundMarkName, mark1, mark2);
break;
} default: {
throw new InvalidRaceDataException("Cannot create CompoundMark from " + compoundMarkElement.toString());
}
}
return compoundMark;
}
/**
* Gets a mark from an Element.
* @param mark The {@link Element} describing the {@link Mark}.
* @return The {@link Mark}.
*/
private Mark createMark(Element mark) {
//Source ID.
int sourceID = Integer.parseInt(mark.getAttribute("SourceID"));
//Name.
String name = mark.getAttribute("Name");
//Latitude.
double latitude = Double.parseDouble(mark.getAttribute("TargetLat"));
//Longitude.
double longitude = Double.parseDouble(mark.getAttribute("TargetLng"));
//Create mark.
return new Mark(sourceID, name, new GPSCoordinate(latitude, longitude));
}
/**
* 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 a CompoundMark element.
* @param element The CompoundMark element with a "Name" attribute.
* @return value of "name" attribute.
*/
private String getCompoundMarkName(Element element) {
return element.getAttribute("Name");
}
/**
* Populates list of legs given CompoundMarkSequence element and referenced CompoundMark elements.
*/
private void readCompoundMarkSequence() {
//The "<CompoundMarkSequence>...</...>" element. This contains a sequence of Corner elements.
Element compoundMarkSequence = (Element) doc.getElementsByTagName("CompoundMarkSequence").item(0);
//Gets the list of Corner elements.
NodeList corners = compoundMarkSequence.getElementsByTagName("Corner");
//Gets the first corner.
Element cornerElement = (Element)corners.item(0);
//Gets the ID number of this corner element.
int cornerID = getCompoundMarkID(cornerElement);
//Gets the CompoundMark associated with this corner.
CompoundMark lastCompoundMark = this.compoundMarkMap.get(cornerID);
//The name of the leg is the name of the first compoundMark in the leg.
String legName = lastCompoundMark.getName();
//For each following corner, create a leg between cornerN and cornerN+1.
for(int i = 1; i < corners.getLength(); i++) {
//Gets the next corner element.
cornerElement = (Element) corners.item(i);
//Gets the ID number of this corner element.
cornerID = getCompoundMarkID(cornerElement);
//Gets the CompoundMark associated with this corner.
CompoundMark currentCompoundMark = this.compoundMarkMap.get(cornerID);
//Create a leg from these two adjacent compound marks.
Leg leg = new Leg(legName, lastCompoundMark, currentCompoundMark, i - 1);
legs.add(leg);
//Prepare for next iteration.
lastCompoundMark = currentCompoundMark;
legName = lastCompoundMark.getName();
}
}
/**
* Reads the boundary limits of the course.
*/
private void readCourseLimits() {
//The "<CourseLimit>...</...>" element. This contains a sequence of Limit elements.
Element courseLimit = (Element) doc.getElementsByTagName("CourseLimit").item(0);
//For each limit element...
for(int i = 0; i < courseLimit.getChildNodes().getLength(); i++) {
//Get the Limit element.
Element limit = (Element) courseLimit.getChildNodes().item(i);
//If it is actually a Limit element, add the limit to boundary list.
if (limit.getNodeName().equals("Limit")) {
double latitude = Double.parseDouble(limit.getAttribute("Lat"));
double longitude = Double.parseDouble(limit.getAttribute("Lon"));
boundary.add(new GPSCoordinate(latitude, longitude));
}
}
}
/**
* Reads the gps coordinate of the top left of the map, using the course limits.
*/
private void readMapTopLeft(){
double minLatitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude();
double minLongitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude();
mapTopLeft = new GPSCoordinate(minLatitude, minLongitude);
}
/**
* Reads the gps coordinate of the bottom right of the map, using the course limits.
*/
private void readMapBottomRight(){
double maxLatitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude();
double maxLongitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude();
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<CompoundMark> getCompoundMarks() {
return new ArrayList<>(compoundMarkMap.values());
}
public ZonedDateTime getCreationDateTime() {
return creationTimeDate;
}
public ZonedDateTime getStartDateTime() {
return raceStartTime;
}
public int getRaceId() {
return raceID;
}
public RaceTypeEnum getRaceType() {
return raceType;
}
public boolean getPostponed() {
return postpone;
}
public List<Integer> getParticipants() {
return participants;
}
}

@ -0,0 +1,70 @@
package shared.dataInput;
/**
* Provides information about a race regatta.
*/
public interface RegattaDataSource {
/**
* Returns the ID of the regatta.
* @return The ID of the regatta.
*/
int getRegattaID();
/**
* Returns the name of the regatta.
* @return The name of the regatta.
*/
String getRegattaName();
/**
* Returns the ID of the race this regatta relates to.
* @return The ID of the race that this regatta relates to.
*/
int getRaceID();
/**
* Returns the name of the course.
* @return
*/
String getCourseName();
/**
* Returns the latitude of the centre of the course.
* @return The latitude of the centre of the course.
*/
double getCentralLatitude();
/**
* Returns the longitude of the centre of the course.
* @return The longitude of the centre of the course.
*/
double getCentralLongitude();
/**
* Returns the altitude of the centre of the course.
* @return The altitude of the centre of the course.
*/
double getCentralAltitude();
/**
* Returns the UTC offset of the course's location.
* @return The UTC offset of the course.
*/
float getUtcOffset();
/**
* Returns the magnetic variation of the course's location.
* @return The magnetic variation of the course.
*/
float getMagneticVariation();
}

@ -4,6 +4,7 @@ import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import shared.dataInput.XMLReader;
import shared.exceptions.InvalidRegattaDataException;
import shared.exceptions.XMLReaderException;
import shared.model.GPSCoordinate;
@ -14,7 +15,7 @@ import java.io.InputStream;
/**
* XML reader class for regatta xml file.
*/
public class RegattaXMLReader extends XMLReader {
public class RegattaXMLReader extends XMLReader implements RegattaDataSource {
/**
* The regatta ID.
*/
@ -63,26 +64,40 @@ public class RegattaXMLReader extends XMLReader {
/**
* Constructor for Regatta XML
* Constructor for Regatta XML using a file read as a resource.
*
* @param filePath path of the file to read. Read as a resource.
* @throws XMLReaderException Thrown if the file cannot be parsed.
* @throws InvalidRegattaDataException Thrown if the file cannot be parsed correctly.
*/
public RegattaXMLReader(String filePath) throws XMLReaderException {
public RegattaXMLReader(String filePath) throws XMLReaderException, InvalidRegattaDataException {
super(filePath);
//Attempt to read boat xml file.
try {
read();
} catch (Exception e) {
throw new InvalidRegattaDataException("An error occurred while reading the regatta xml file", e);
}
}
/**
* Alternate Constructor that takes in an inputstream instead
* @param xmlString Input stream of the XML
* Constructor for Regatta XML using an InputStream.
* @param xmlString Input stream of the XML.
* @throws XMLReaderException Thrown if the input stream cannot be parsed.
* @throws InvalidRegattaDataException Thrown if the stream cannot be parsed correctly.
*/
public RegattaXMLReader(InputStream xmlString) throws XMLReaderException {
public RegattaXMLReader(InputStream xmlString) throws XMLReaderException, InvalidRegattaDataException {
super(xmlString);
//Attempt to read boat xml file.
try {
read();
} catch (Exception e) {
throw new InvalidRegattaDataException("An error occurred while reading the regatta xml stream", e);
}
}
@ -103,6 +118,8 @@ public class RegattaXMLReader extends XMLReader {
this.regattaID = Integer.parseInt(getTextValueOfNode(attributes, "RegattaID"));
this.regattaName = getTextValueOfNode(attributes, "RegattaName");
//this.raceID = Integer.parseInt(getTextValueOfNode(attributes, "RaceID"));
this.courseName = getTextValueOfNode(attributes, "CourseName");
this.centralLatitude = Double.parseDouble(getTextValueOfNode(attributes, "CentralLatitude"));
@ -187,6 +204,10 @@ public class RegattaXMLReader extends XMLReader {
this.magneticVariation = magneticVariation;
}
/**
* Returns the GPS coorindates of the centre of the regatta.
* @return The gps coordinate for the centre of the regatta.
*/
public GPSCoordinate getGPSCoordinate() {
return new GPSCoordinate(centralLatitude, centralLongitude);
}

@ -1,14 +1,15 @@
package mock.exceptions;
package shared.exceptions;
/**
* An exception thrown when we cannot generate Boats.xml and send an XML message.
* An exception thrown when we cannot generate Boats.xml and send an XML message, or we cannot parse a Boats.xml file.
*/
public class InvalidBoatDataException extends RuntimeException {
public InvalidBoatDataException() {
}
public InvalidBoatDataException(String message) {
super(message);
}
public InvalidBoatDataException(String message, Throwable cause) {
super(message, cause);
}
}

@ -1,13 +1,15 @@
package mock.exceptions;
package shared.exceptions;
/**
* Exception thrown when we cannot generate Race.xml data, and send an XML message.
* Exception thrown when we cannot generate Race.xml data, and send an XML message, or we cannot parse a Race.xml file.
*/
public class InvalidRaceDataException extends RuntimeException {
public InvalidRaceDataException() {
}
public InvalidRaceDataException(String message) {
super(message);
}
public InvalidRaceDataException(String message, Throwable cause) {
super(message, cause);
}
}

@ -0,0 +1,15 @@
package shared.exceptions;
/**
* An exception thrown when we cannot generate Regatta.xml and send an XML message, or we cannot parse a Regatta.xml file.
*/
public class InvalidRegattaDataException extends RuntimeException {
public InvalidRegattaDataException(String message) {
super(message);
}
public InvalidRegattaDataException(String message, Throwable cause) {
super(message, cause);
}
}

@ -127,4 +127,23 @@ public class Angle implements Comparable<Angle> {
return angle;
}
/**
* Sets the degrees value of the angle.
* @param degrees New value of the angle.
*/
public void setDegrees(double degrees) {
this.degrees = degrees;
}
/**
* Sets the radians value of the angle.
* @param radians New value of the angle.
*/
public void setRadians(double radians) {
this.setDegrees(Math.toDegrees(radians));
}
}

@ -65,4 +65,45 @@ public class Azimuth extends Angle{
return Azimuth.fromDegrees(bearing.degrees());
}
/**
* Constructs an Azimuth object from another Azimuth object.
* @param azimuth Azimuth object to read value from.
* @return Azimuth object.
*/
public static Azimuth fromAzimuth(Azimuth azimuth) {
return Azimuth.fromDegrees(azimuth.degrees());
}
/**
* Sets the degrees value of the azimuth.
* @param degrees New value of the azimuth.
*/
public void setDegrees(double degrees) {
//Put degree value in correct interval.
degrees = Azimuth.toAzimuthInterval(degrees);
//Update.
super.setDegrees(degrees);
}
/**
* Sets the radians value of the azimuth.
* @param radians New value of the azimuth.
*/
public void setRadians(double radians) {
this.setDegrees(Math.toDegrees(radians));
}
/**
* Sets the value of this azimuth from another azimuth.
* @param azimuth Azimuth to copy the value from.
*/
public void setAzimuth(Azimuth azimuth) {
this.setDegrees(azimuth.degrees());
}
}

@ -28,7 +28,7 @@ public class Bearing extends Angle {
*/
public static double toBearingInterval(double degrees) {
return Angle.toPeriodicInterval(degrees, -0d, 360d, 360d);
return Angle.toPeriodicInterval(degrees, 0d, 360d, 360d);
}
/**
@ -63,4 +63,45 @@ public class Bearing extends Angle {
return Bearing.fromDegrees(azimuth.degrees());
}
/**
* Constructs a Bearing object from another Bearing object.
* This can be used to copy a bearing.
* @param bearing Bearing object to read value from.
* @return Bearing object.
*/
public static Bearing fromBearing(Bearing bearing) {
return Bearing.fromDegrees(bearing.degrees());
}
/**
* Sets the degrees value of the bearing.
* @param degrees New value of the bearing.
*/
public void setDegrees(double degrees) {
//Put degree value in correct interval.
degrees = Bearing.toBearingInterval(degrees);
//Update.
super.setDegrees(degrees);
}
/**
* Sets the radians value of the bearing.
* @param radians New value of the bearing.
*/
public void setRadians(double radians) {
this.setDegrees(Math.toDegrees(radians));
}
/**
* Sets the value of this bearing from another bearing.
* @param bearing Bearing to copy the value from.
*/
public void setBearing(Bearing bearing) {
this.setDegrees(bearing.degrees());
}
}

@ -1,12 +1,13 @@
package shared.model;
import javafx.beans.property.StringProperty;
import network.Messages.Enums.BoatStatusEnum;
/**
* Boat Model that is used to store information on the boats that are running in the race.
*/
public class Boat {
public abstract class Boat {
/**
* The name of the boat/team.
*/
@ -50,6 +51,11 @@ public class Boat {
*/
private double distanceTravelledInLeg;
/**
* The boat's position within the race (e.g., 5th).
*/
private StringProperty positionInRace;
/**
* The time, in milliseconds, that has elapsed during the current leg.
* TODO milliseconds
@ -69,7 +75,9 @@ public class Boat {
private BoatStatusEnum status;
/**
* The amount of time, in seconds, until the boat reaches the next mark.
*/
private long estimatedTime = 0;
@ -197,6 +205,19 @@ public class Boat {
}
public StringProperty positionProperty() {
return positionInRace;
}
public void setPosition(String position) {
this.positionInRace.set(position);
}
public String getPosition() {
return this.positionInRace.get();
}
/**
* Returns the current position of the boat.
* @return The current position of the boat.

@ -6,6 +6,16 @@ package shared.model;
*/
public class CompoundMark {
/**
* The ID of the compound mark.
*/
private int id;
/**
* The name of the compound mark.
*/
private String name;
/**
* The first mark in the compound mark.
*/
@ -26,7 +36,9 @@ public class CompoundMark {
* Constructs a compound mark from a single mark.
* @param mark1 The individual mark that comprises this compound mark.
*/
public CompoundMark(Mark mark1) {
public CompoundMark(int id, String name, Mark mark1) {
this.id = id;
this.name = name;
this.mark1 = mark1;
this.averageGPSCoordinate = calculateAverage();
@ -38,7 +50,9 @@ public class CompoundMark {
* @param mark1 The first individual mark that comprises this compound mark.
* @param mark2 The second individual mark that comprises this compound mark.
*/
public CompoundMark(Mark mark1, Mark mark2) {
public CompoundMark(int id, String name, Mark mark1, Mark mark2) {
this.id = id;
this.name = name;
this.mark1 = mark1;
this.mark2 = mark2;
this.averageGPSCoordinate = calculateAverage();
@ -46,6 +60,22 @@ public class CompoundMark {
}
/**
* Returns the ID of this compound mark.
* @return The ID of this compound mark.
*/
public int getId() {
return id;
}
/**
* Returns the name of this compound mark
* @return The name of this compound mark.
*/
public String getName() {
return name;
}
/**
* Returns the first mark of the compound mark.
* @return The first mark of the compound mark.

@ -60,5 +60,11 @@ public class Mark {
return position;
}
/**
* Sets the position of the mark to a specified GPSCoordinate.
* @param position The new GPSCoordinate to use.
*/
public void setPosition(GPSCoordinate position) {
this.position = position;
}
}

@ -0,0 +1,184 @@
package shared.model;
import javafx.animation.AnimationTimer;
import javafx.collections.FXCollections;
import mock.model.VMG;
import network.Messages.Enums.RaceStatusEnum;
import network.Messages.Enums.RaceTypeEnum;
import shared.dataInput.BoatDataSource;
import shared.dataInput.RaceDataSource;
import shared.dataInput.RegattaDataSource;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import static java.lang.Math.cos;
/**
* Represents a yacht race.
* This is a base class inherited by {@link mock.model.MockRace} and {@link visualiser.model.VisualiserRace}.
* Has a course, state, wind, boundaries, etc.... Boats are added by inheriting classes (see {@link Boat}, {@link mock.model.MockBoat}, {@link visualiser.model.VisualiserBoat}.
*/
public abstract class Race implements Runnable {
/**
* A list of compound marks in the race.
*/
protected List<CompoundMark> compoundMarks;
/**
* A list of legs in the race.
*/
protected List<Leg> legs;
/**
* A list of coordinates describing the boundary of the course.
*/
protected List<GPSCoordinate> boundary;
/**
* The elapsed time, in milliseconds, of the race.
*/
protected long totalTimeElapsed;
/**
* The starting timestamp, in milliseconds, of the race.
*/
protected long startTime;
/**
* The race ID of the course.
*/
protected int raceId;
/**
* The current status of the race.
*/
protected RaceStatusEnum raceStatusEnum;
/**
* The type of race this is.
*/
protected RaceTypeEnum raceType;
/**
* The current wind direction bearing.
*/
protected Bearing windDirection;
/**
* Wind speed (knots).
* Convert this to millimeters per second before passing to RaceStatus.
*/
protected double windSpeed;
/**
* Constructs a race object with a given BoatDataSource, RaceDataSource, and RegattaDataSource.
* @param boatDataSource Data source for boat related data (yachts and marker boats).
* @param raceDataSource Data source for race related data (participating boats, legs, etc...).
* @param regattaDataSource Data source for race related data (course name, location, timezone, etc...).
*/
public Race(BoatDataSource boatDataSource, RaceDataSource raceDataSource, RegattaDataSource regattaDataSource) {
this.compoundMarks = raceDataSource.getCompoundMarks();
this.boundary = raceDataSource.getBoundary();
//We add a "dummy" leg at the end of the race.
this.legs = raceDataSource.getLegs();
this.legs.add(new Leg("Finish", this.legs.size()));
this.raceId = raceDataSource.getRaceId();
this.startTime = raceDataSource.getStartDateTime().toInstant().toEpochMilli();
this.setRaceStatusEnum(RaceStatusEnum.NOT_ACTIVE);
this.raceType = raceDataSource.getRaceType();
this.windSpeed = 0;
this.windDirection = Bearing.fromDegrees(0);
this.totalTimeElapsed = 0;
}
/**
* Initialise the boats in the race.
* This sets their starting positions and current legs.
*/
protected abstract void initialiseBoats();
/**
* Determines whether or not a specific leg is the last leg in the race.
* @param leg The leg to check.
* @return Returns true if it is the last, false otherwise.
*/
protected boolean isLastLeg(Leg leg) {
//Get the last leg.
Leg lastLeg = this.legs.get(this.legs.size() - 1);
//Check its ID.
int lastLegID = lastLeg.getLegNumber();
//Get the specified leg's ID.
int legID = leg.getLegNumber();
//Check if they are the same.
return legID == lastLegID;
}
/**
* Returns the current race status.
* @return The current race status.
*/
public RaceStatusEnum getRaceStatusEnum() {
return raceStatusEnum;
}
/**
* Sets the current race status.
* @param raceStatusEnum The new status of the race.
*/
protected void setRaceStatusEnum(RaceStatusEnum raceStatusEnum) {
this.raceStatusEnum = raceStatusEnum;
}
/**
* Returns the type of race this is.
* @return The type of race this is.
*/
public RaceTypeEnum getRaceType() {
return raceType;
}
}

@ -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;
}
}

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<BoatConfig>
<Boats>
<!--Mark Boats-->
<Boat Type="Mark" BoatName="PRO" SourceID="101" >
<GPSposition X= "-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat Type="Mark" BoatName="PIN" SourceID="102" >
<GPSposition X= "-64.855242" Y="32.293771" Z="0"/>
</Boat>
<Boat Type="Mark" BoatName="Marker1" SourceID="103" >
<GPSposition X= "-64.843983" Y="32.293039" Z="0"/>
</Boat>
<Boat Type="Mark" BoatName="WGL" SourceID="104" >
<GPSposition X= "-64.850045" Y="32.28468" Z="0"/>
</Boat>
<Boat Type="Mark" BoatName="WGR" SourceID="105" >
<GPSposition X= "-64.847591" Y="32.280164" Z="0"/>
</Boat>
<Boat Type="Mark" BoatName="LGL" SourceID="106" >
<GPSposition X= "-64.835249" Y="32.309693" Z="0"/>
</Boat>
<Boat Type="Mark" BoatName="LGR" SourceID="107" >
<GPSposition X= "-64.831785" Y="32.308046" Z="0"/>
</Boat>
<Boat Type="Mark" BoatName="FL" SourceID="108" >
<GPSposition X= "-64.839291" Y="32.317379" Z="0"/>
</Boat>
<Boat Type="Mark" BoatName="FR" SourceID="109" >
<GPSposition X= "-64.83626" Y="32.317257" Z="0"/>
</Boat>
<!--Participants-->
<Boat BoatName="Team ORACLE USA" HullNum="RG01" ShapeID="0" ShortName="USA" SourceID="121" StoweName="USA" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Land Rover BAR" HullNum="RG01" ShapeID="0" ShortName="GBR" SourceID="122" StoweName="GBR" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="SoftBank Team Japan" HullNum="RG01" ShapeID="0" ShortName="JPN" SourceID="123" StoweName="JPN" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Groupama Team France" HullNum="RG01" ShapeID="0" ShortName="FRA" SourceID="124" StoweName="FRA" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Artemis Racing" HullNum="RG01" ShapeID="0" ShortName="SWE" SourceID="125" StoweName="SWE" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
<Boat BoatName="Emirates Team New Zealand" HullNum="RG01" ShapeID="0" ShortName="NZL" SourceID="126" StoweName="NZL" Type="Yacht">
<GPSposition X="-64.854304" Y="32.296577" Z="0"/>
</Boat>
</Boats>
</BoatConfig>

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Race>
<RaceID>5326</RaceID>
<RaceType>FLEET</RaceType>
<CreationTimeDate>CREATION_TIME</CreationTimeDate>
<RaceStartTime Postpone="false" Time="START_TIME"/>
<Participants>
<Yacht SourceID="121"/>
<Yacht SourceID="122"/>
<Yacht SourceID="123"/>
<Yacht SourceID="124"/>
<Yacht SourceID="125"/>
<Yacht SourceID="126"/>
</Participants>
<CompoundMarkSequence>
<Corner CompoundMarkID="1" SeqID="1"/>
<Corner CompoundMarkID="2" SeqID="2"/>
<Corner CompoundMarkID="4" SeqID="3"/>
<Corner CompoundMarkID="3" SeqID="4"/>
<Corner CompoundMarkID="4" SeqID="5"/>
<Corner CompoundMarkID="5" SeqID="6"/>
</CompoundMarkSequence>
<Course>
<CompoundMark CompoundMarkID="1" Name="Start Line">
<Mark SeqId="1" Name="PRO" TargetLat="32.296577" TargetLng="-64.854304" SourceID="101"/>
<Mark SeqId="2" Name="PIN" TargetLat="32.293771" TargetLng="-64.855242" SourceID="102"/>
</CompoundMark>
<CompoundMark CompoundMarkID="2" Name="Marker 1">
<Mark Name="Marker1" TargetLat="32.293039" TargetLng="-64.843983" SourceID="103"/>
</CompoundMark>
<CompoundMark CompoundMarkID="3" Name="Windward Gate">
<Mark Name="WGL" SeqId="1" TargetLat="32.28468" TargetLng="-64.850045" SourceID="104"/>
<Mark Name="WGR" SeqId="2" TargetLat="32.280164" TargetLng="-64.847591" SourceID="105"/>
</CompoundMark>
<CompoundMark CompoundMarkID="4" Name="Leeward Gate">
<Mark Name="LGL" SeqId="1" TargetLat="32.309693" TargetLng="-64.835249" SourceID="106"/>
<Mark Name="LGR" SeqId="2" TargetLat="32.308046" TargetLng="-64.831785" SourceID="107"/>
</CompoundMark>
<CompoundMark CompoundMarkID="5" Name="Finish Line">
<Mark Name="FL" SeqId="1" TargetLat="32.317379" TargetLng="-64.839291" SourceID="108"/>
<Mark Name="FR" SeqId="2" TargetLat="32.317257" TargetLng="-64.83626" SourceID="109"/>
</CompoundMark>
</Course>
<CourseLimit>
<Limit Lat="32.313922" Lon="-64.837168" SeqID="1"/>
<Limit Lat="32.317379" Lon="-64.839291" SeqID="2"/>
<Limit Lat="32.317911" Lon="-64.836996" SeqID="3"/>
<Limit Lat="32.317257" Lon="-64.83626" SeqID="4"/>
<Limit Lat="32.304273" Lon="-64.822834" SeqID="5"/>
<Limit Lat="32.279097" Lon="-64.841545" SeqID="6"/>
<Limit Lat="32.279604" Lon="-64.849871" SeqID="7"/>
<Limit Lat="32.289545" Lon="-64.854162" SeqID="8"/>
<Limit Lat="32.290198" Lon="-64.858711" SeqID="9"/>
<Limit Lat="32.297164" Lon="-64.856394" SeqID="10"/>
<Limit Lat="32.296148" Lon="-64.849184" SeqID="11"/>
</CourseLimit>
</Race>

@ -0,0 +1,10 @@
<RegattaConfig>
<RegattaID>3</RegattaID>
<RegattaName>New Zealand Test</RegattaName>
<CourseName>North Head</CourseName>
<CentralLatitude>-36.82791529</CentralLatitude>
<CentralLongitude>174.81218919</CentralLongitude>
<CentralAltitude>0.00</CentralAltitude>
<UtcOffset>12</UtcOffset>
<MagneticVariation>14.1</MagneticVariation>
</RegattaConfig>

@ -0,0 +1,8 @@
Tws,Twa0,Bsp0,Twa1,Bsp1,UpTwa,UpBsp,Twa2,Bsp2,Twa3,Bsp3,Twa4,Bsp4,Twa5,Bsp5,Twa6,Bsp6,DnTwa,DnBsp,Twa7,Bsp7
4,0,0,30,4,45,8,60,9,75,10,90,10,115,10,145,10,155,10,175,4
8,0,0,30,7,43,10,60,11,75,11,90,11,115,12,145,12,153,12,175,10
12,0,0,30,11,43,14.4,60,16,75,20,90,23,115,24,145,23,153,21.6,175,14
16,0,0,30,12,42,19.2,60,25,75,27,90,31,115,32,145,30,153,28.8,175,20
20,0,0,30,13,41,24,60,29,75,37,90,39,115,40,145,38,153,36,175,24
25,0,0,30,15,40,30,60,38,75,44,90,49,115,50,145,49,151,47,175,30
30,0,0,30,15,42,30,60,37,75,42,90,48,115,49,145,48,150,46,175,32
1 Tws Twa0 Bsp0 Twa1 Bsp1 UpTwa UpBsp Twa2 Bsp2 Twa3 Bsp3 Twa4 Bsp4 Twa5 Bsp5 Twa6 Bsp6 DnTwa DnBsp Twa7 Bsp7
2 4 0 0 30 4 45 8 60 9 75 10 90 10 115 10 145 10 155 10 175 4
3 8 0 0 30 7 43 10 60 11 75 11 90 11 115 12 145 12 153 12 175 10
4 12 0 0 30 11 43 14.4 60 16 75 20 90 23 115 24 145 23 153 21.6 175 14
5 16 0 0 30 12 42 19.2 60 25 75 27 90 31 115 32 145 30 153 28.8 175 20
6 20 0 0 30 13 41 24 60 29 75 37 90 39 115 40 145 38 153 36 175 24
7 25 0 0 30 15 40 30 60 38 75 44 90 49 115 50 145 49 151 47 175 30
8 30 0 0 30 15 42 30 60 37 75 42 90 48 115 49 145 48 150 46 175 32
Loading…
Cancel
Save